Universal React With Redux-Saga

Originally written May 2016

My very first project at Red Badger was the rebuild of camdenmarket.com where we used mostly React to build a flexible, content-driven site. There are many parts of our code base that I’m proud of but one piece I’m particularly excited about is our use of React, Redux and Redux-Saga to fetch data and inject it into the state of our isomorphic app entirely from the server side.

Universal Javascript apps are nothing new and have a ton of benefits. Our use of Redux-Saga to pre-load state before ever rendering a component is an elegant, testable implementation of this very neat concept. No hooking into component lifecycle! No AJAX calls! No stubbing out promises inside your tests! If you’re already using sagas to fetch data during a component’s lifecycle, why not try using them to do server-side data fetching?

As a simple example, imagine we have a ProductPage container component that does essentially nothing except render a list of products. Our goal is to fetch the product data from an API and inject it into the state of the application on the server side so that it’s available immediately when we render the page.

class ProductPage extends Component {
  static preload() {
return [
[fetchProducts]
];
}
  render() {
return (<ProductList products={this.props.products} />);
}
}

Inside our component we’ve added a preload() function that returns all the sagas we want to run before this component renders. As you can probably guess, the fetchProducts saga asynchronously calls an API to get some products for our product list and then renders that on the page.

export function* fetchProducts() {
try {
const products = yield call(getDataFromAPI, ‘products’);
yield put({ type:’PRODUCTS_FETCH_SUCCESS’, result:products });
} catch (e) {
yield put(errorActions.handleError(e));
}
}

If you’re not too familiar with sagas or Redux don’t worry too much about the details here — just know that getDataFromAPI is our async data fetching call and that the saga is a generator function that steps through each line, waiting for each to complete before continuing. The put() that happens on success emits a Redux action that is handled by our reducer and puts the products into the application’s state.

Inside our server we use React-Router to do server-side routing.

app.get(“*”, (req, res) => {
match({ routes, location: req.url },
(error, redirectLocation, renderProps) => {
renderPageWithInitialState(renderProps)(req, res);
}
});

We pass renderProps — a property of the route-matching callback that contains all the information about the component we’re trying to load — to a helper function renderPageWithInitialState().

export function renderPageWithInitialState(renderProps) {
return async function (req, res) {
const { store, sagaMiddleware } = mainStore();
const preloaders = getPreloaders(renderProps);
    sagaMiddleware.run(
waitAll(preloaders)
).done.then(() => {
res.status(200).send(renderPage(renderProps, store));
});
};
}

There’s quite a lot going on in that block: mainStore() is a wrapper function for Redux’s createStore() ability — inside we add our main reducer, some initial state (defaulting to an empty object), and create and apply saga middleware using createSagaMiddleware() from React-Saga. We pass back both the newly-created store and the saga middleware so we can use them down the line. The getPreloaders() function is a nifty helper that parses the renderProps argument and digs out the sagas we specified in the component’s preload() function.

Once we have the store, saga middleware, and list of sagas to preload, we can go ahead and start running them, using waitAll() as an intermediary to map the sagas into runnable tasks.

import { fork, join } from ‘redux-saga/effects’;
export default const waitAll = (sagas) => function* () {
const tasks = yield sagas.map(
([saga, …params]) => fork(saga, …params));
yield tasks.map(join);
};

Again, if you’re not very familiar with sagas, don’t be put off by this dense code block — the result of this function is a set of runnable sagas that we can know the state of. Basically, we just need to be able to know when all the sagas (just our async data fetches) are done so that we can carry on and render the page with the data we fetched.

Because we have access to the store in the renderPageWithInitialState() function on the server, when all the sagas have finished running we get all the data that we need to render our component — without having ever tried to load anything on the client side.

After all the sagas have run we set the state to be accessible on the client side and to begin the rendering process.

export function renderPage(initialState) {
return `
<!doctype html>
<html>
<head>
<title>Cool Shopping App</title>
</head>
<body>
<script>
window.__INITIAL_STATE__ = ${JSON.stringify(initialState)}
</script>
<script src=”${CLIENT_APP_ENDPOINT}/main.js”></script>
</body>
</html>
`;
}

Setting the store on the window object means we can access it in the client-side script we’ve just loaded, main.js.

const initialState = window.__INITIAL_STATE__;
const { store } = mainStore(initialState);
render(
<Provider store={store}>{routes}</Provider>,
document.getElementById(“root”)
);

Again we use mainStore(), but this time we already have our initial state, ready and waiting to be passed in and trickled down to any components that will be rendered on the page. And that’s it! Speedy server-side fetching of data that we need to render any page.