2. Making components get their own data

So far our application has all the data for all the modules right from the start. It's convenient for now, but not very practical in real world. Imagine Twitter of Facebook loading ALL the data at once! We need to move towards our final goal - server-rendered website that gets data from API. We also don't want to change our code every time we add a new module.

It seems that modules will have to take of themselves - fetching data, updating data and so on.

Now all of our modules get their data through props when they are rendered, but in the future they will have to get it from RESTfull API. We want to teach our modules not to rely on data being available right away, but instead take an extra step of fetching it. Data will ultimately come from the same /shared/data.json as before, but we will lay the groundwork for future async communication with backend API .

Let's think how we can achieve that. First of all, every module will have to make explicit call for data - looks we will be dispatching some actions finally. Then data will be fetched somehow and the state of our store will be updated accordingly - something reducers are there to do.

First of all, let's update /shared/Root.jsx, where now we can find this code:

...
import state from './data.json';
const store = configureStore(state);
...

Here we import all data from JSON file and pass it to configureStore function as app's initial state. Let's change it to this:

...
const store = configureStore();
...

Now we don't have any data to render. Our routes will still work, if we manually navigate to /blog we will see H1 tag, but no blog posts obviously. This works because we have initialState variables in reducers - app's state is empty, but maintains correct structure:

const initialState = {
    menu: []
};

export default function app(state = initialState, action = {}) {
    ...
}

store.getState().app.menu is an empty array and call to map function in /shared/modules/app/components/Menu.jsx will not throw an error.

Ok, we've removed data from our app, now what?

First - let's make our modules call for data. Good place to do it is HoC - that's what they're for. React provides us with lifecycle methods, modules need data when they are rendered, so HoC's componentDidMount() seems like a good place to start.

With "were" question answered, it's time for "how". How can we fetch data from components method?

We could somehow expose our data storage (contents of /shared/data.json in this case) to all HoCs and make them communicate directly with it. This means that after receiving data HoCs will have to store it locally, making themselves stateful or jump through many hoops trying to update redux store. This doesn't seem like a good idea, so - onwards!

Another (and tempting) way to fetch data is inside reducers. Reducer accepts an action, fetches data and dispatches another action with results - seems cool! But Redux creator Dan Abramov argues that this is antipattern (and we should agree with him completely). Redux won't even allow us to do this. It will raise an error on the attempt to dispatch an action from the reducer. (And I'm not even talking about exposing data storage and dispatch functions to the reducers).

Our quest brings us to Redux's concept of middlewares. If we imagine the path that action makes from a dispatch call to a reducer as an assembly line, the middlewares would be workers/machines. They can perform some work on an item passing the assembly line, adding or removing something from it.

Assembly line

(note my mad Photoshop skilz)

Every action passes every middleware that developer has chosen to use. Any middleware can:

  1. modify an action;

  2. do not modify an action, but do something around it, e.g. log it to the console (this is what redux-logger is basically doing);

  3. intercept an action and prevent it from reaching reducers at all;

  4. dispatch other actions.

Points 3 and 4 look promising. We can create a middleware that will look for actions of a particular type, intercept them, fetch data and dispatch the action with response. That action be processed by appropriate reducer.

But before we start coding, let's remember #2 of our goals. "introducing new functionality (e.g. partners) should not require any modification of core files". We didn't provide any definition of which of the files should be considered "core" (always a right strategy when in doubt, lawmakers do it all the time), but this crucial middleware should definitely fall into that category.

So, we won't be able to modify this middleware every time we add a new module. This will apply some restrictions on the actions that it will intercept and dispatch:

  • all actions that modules will dispatch will have the same type;
  • they will include the type of action that will be dispatched with result, so that result finds it's way to correct module.

Let's see, what action our galleries module could dispatch in order to get data:

{
    "type": "DATA_PLEASE",
    "resultType": "GALLERIES_DATA",
    "key": "gallery"
}

Here we have type field, resultType field (resulting action will have this type) and key field, which indicates what part of data our module is interested in. In our case data will still come from /shared/data.json so key field should correspond to the field of JSON in that file.

After this action is dispatched by the module, it's intercepted in our middleware. Requested data is somehow gathered and new action with type GALLERIES_DATA is dispatched. It's then is processed by reducer in /shared/modules/galleries/reducer.js.

Let's see how this middleware may look like: /shared/redux/dataMiddleware.js

import data from '../data.json';

export default store => next => action => {
    if (action.type !== 'DATA_PLEASE') {
        return next(action);
    }

    next({
        type: action.returnType,
        data: data[action.key]
    });
};

We import data from /shared/data.json. Middleware function will have access to it as a closure.

Then we check the type of any incoming action with if (action.type !== 'DATA_PLEASE'). If it's an action we need, we pass further down middleware chain with return next(action) (we just call next middleware). If this is the droid action we are looking for - we don't let it go to next middleware, preventing it from reaching reducers. Instead we dispatch another action. The type of this action will be taken from imcoming action's resultType field. It's data field will contain the part of our whole data that module requested.

The most interesting thing here is how our middleware in defined: store => next => action => { ... }. Translating it back to good ol' ES5 we get:

function (store) {
    return function (next) {
        return function (action) { ... }
    }
}

It's a function that returns a function that returns a function. If your brain just stopped working like mine did when I first saw something like this, it might be useful to take some time and read about currying and partial application in JavaScript (http://benalman.com/news/2012/09/partial-application-in-javascript/)

In the end the trick is that innermost function is called with action as an argument and with access to next and store variables as a closure.

Now let's create an action that galleries HoC will dispatch:

/shared/modules/galleries/actions.js

export const fetchGalleriesData = () => ({
    type: 'DATA_PLEASE',
    returnType: 'GALLERIES_DATA_FETCHED',
    key: 'galleries'
});

Make HoC dispatch it:

/shared/modules/galleries/container.js

...
import { fetchGalleriesData } from './actions.js';

class GalleriesContainer extends React.Component {
    componentDidMount() {
        this.props.dispatch(fetchGalleriesData());
    }

    render() {
        return (
            <GalleriesList galleries={this.props.galleries}/>
        );
    }
}
...

Update galleries module's reducer to process events with type GALLERIES_DATA:

/shared/modules/galleries/reducer.js

const initialState = [];

export default function galleries(state = initialState, action = {}) {
    let newState;

    switch(action.type) {
        case 'GALLERIES_DATA_FETCHED':
            newState = action.data;
            break;

        default:
            newState = state;
            break;
    }

    return newState;
}

Last thing we need to do is to add our middleware into redux store:

...
import dataMiddleware from './dataMiddleware.js';

export default function configureStore(initialState) {
    const middleware = [
        routerMiddleware(browserHistory),
        dataMiddleware
    ];

    ...
}
...

And that's it! Our galleries module now fetches data and displays galleries like before.

Modules now can fetch data for themselves by dispatching an action of unified format. The middleware processes these actions without knowing anything about the modules or the data they need.

You can find all code of this section here: https://github.com/graymur/another-react-universtal-tutorial. Clone the repo, switch to branch "2", run npm install and npm start commands and navigate to http://localhost:3000. You will also need babel-node installed.

results matching ""

    No results matching ""