5. Abstracting the API handlers

Let's recap the problem: we have a middleware that handles HTTP calls to our backend API. It works fine in the browser, but on the server we have to make HTTP calls to the server itself to get the data. This adds a lot of latency and will force us to do something ugly to form a correct URL of our server in the middleware.

The optimal solution will be to reuse our server-side api() function on the server in the middleware. This means we have to create separate api() for the browser. First, let's extract fetch() call from our middleware:

/client/api.js

import fetch from 'isomorphic-fetch';

export default (endpoint, data = {}) => {
    let url = `/api/${endpoint}?` + Object.keys(data).map(k => k + '=' + encodeURIComponent(data[k])).join('&');
    return fetch(url).then(response => {
        return response.json();
    });
}

Note that we don't do any error handling here - it will be still done in middlware.

Now we update the middleware:

/shared/redux/dataMiddleware.js

import fetch from 'isomorphic-fetch';
import api from '../../client/api.js';

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

    store.dispatch({ type: 'XHR_STARTED' });

    return api(action.key, action.payload || {})
        .then(data => {
            store.dispatch({ type: 'XHR_SUCCEEDED' });

            next({
                type: action.resultType,
                data: data
            });

            return data;
        }).catch(response => {
            console.log(response);
            next({
                type: 'XHR_FAILED',
                error: response.statusText
            });
        });
};

Here we import our API and replace a call to fetch().

Ok, but what now? We extracted fetch() call, but didn't actually change anything - same requests are made. We can try this trick:

/shared/api.js (doesn't actually exist)

if (isServer()) {
    module.exports = require('../server/api.js');
} else {
    module.exports = require('../client/api.js');
}

Here we leverage CommonJS module system that is supported by Node. With it we can make conditional imports/exports.

We can write isServer() function that will return true if we're on Node. But in this case we will find contents of /server/data.json in the bundle created by Webpack. Why? Because Webpack executes the code when it creates a bundle and when it does isServer() will return true.

We can try and make another check to detect Webpack, but this just doesn't feel right. Let's try and find some other way.

The code that gets bundled by weback has an entry point in /client/main.jsx. The server code has an entry point in /server/serve.js. Can we define what version of api() function to use in these entry points?

If we're gonna use api() function in the middleware, it will have to make it's way there. Let's update middleware first:

import fetch from 'isomorphic-fetch';

export default api => store => next => action => {
    ...
};

/shared/redux/dataMiddleware.js now returns not the ready-to-use middleware function. It returns a function that accepts api() function as an argument and then returns the middleware function which has access to that api() function.

Remember we talked about a function that returns a function that returns a function? We need to go deeper! It's now a function that returns a function that returns a function that returns a function. We can film "Inception 2" about it!

Ok, now we can use different api() functions in our middleware. We need correct api() function where we apply middleware on redux store - in /shared/redux/configureStore.js:

import { createStore, applyMiddleware, compose } from 'redux';
import { routerMiddleware } from 'react-router-redux';
import { combineReducers } from 'redux';
import { browserHistory } from 'react-router';
import reducer from './rootReducer.js';
import dataMiddleware from './dataMiddleware.js';

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

    const createStoreWithMiddleware = applyMiddleware(...middleware)(createStore);
    return createStoreWithMiddleware(reducer, initialState);
}

Here we add api as a second argument of configureStore function and use it in dataMiddleware() call.

Now we have to update all configureStore calls. On the server it's no problem at all:
/server/serve.js

app.get('*', (req, res) => {
    match({ routes, location: req.originalUrl }, (err, redirect, renderProps) => {
        let store = configureStore(req.state, api);
        ...
    });
});
...

We already have server-side api() function included to handle /api/* calls. We just pass it as an argument to configureStore().

Client-side code it's a bit trickier. Now configureStore() call is located in /shared/Root.jsx. It's actually run on server as well. We need to move this configureStore() call to our client-side entry point - /client/main.js. It will drag Provider component with it, because it needs the store to initialize. Here's our new /client/main.js:

import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import Root from '../shared/Root.jsx';
import api from './api.js';
import configureStore from '../shared/redux/configureStore.js';
let initialState = {};

if (window.__INITIAL_STATE__ !== 'undefined') {
    initialState = window.__INITIAL_STATE__;
}

const store = configureStore(initialState, api);

render((
    <Provider store={store}>
            <Root/>
    </Provider>
), document.getElementById('root'));

Here we:

  • add new imports;
  • call configureStore with api function as second argument;
  • render Provider component with Root as a child.

We also simplified our initialState check - we had to check if window object existed. Now we don't need to do that, because we're sure that this code will run in the browser.

We also have to update /shared/Root.jsx, which becomes much shorter:

import React from 'react';
import { Router, browserHistory } from 'react-router';
import routes from './routes.jsx';

export default function Root() {
    return (
        <Router history={browserHistory}>{routes}</Router>
    );
};

Now our middleware doesn't depend on URL. We reused the same function that handles /api/* calls. All is good.

You can find all code of this section here: https://github.com/graymur/another-react-universtal-tutorial. Clone the repo, switch to branch "5", 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 ""