Asynchronous Server side rendering with React

Written by AvnerSo | Published 2016/10/11
Tech Story Tags: react | javascript | mobx | web-development | reactjs

TLDRvia the TL;DR App

I’ll try to keep this short and simple : This post will go over an implementation of asynchronous server side rendering in a React application. I’ll be using React-Router, Mobx and Koa. The application code is over-simplified and meant to be minimal — it’s just a demonstration.

Why is this a thing ?

React and React-Router come with out-of-the-box server side rendering. Unfortunately — they do not support asynchronous rendering. That means your routes will be handled on the server and your views will be rendered, but if you need to load any data that isn’t on the server (e.g. from a database), that data won’t be available at the time the server does the rendering.

When is this needed ?

Whenever your application needs some dynamic content (e.g. data from an API) before the pages initially loads. In our example — we’ll have a page that shows information about a comic book character. That information comes from Wikipedia’s API. If that page was shared on Facebook, the Facebook crawler would expect the data about that character to be in the initial response that it gets from the server. If the dynamic data is only rendered on the client side — it would be missing from our Facebook post.

Synchronous Rendering ;)

Let’s dive in

You can find all this code in https://github.com/avnersorek/react-async-server-side-rendering. I’ll include some code snippets but I recommend using the links to the repository to get a better view.

Our example application will have two views — the root view (‘/’) which shows a list of comic book characters created by Stan Lee, and another view (‘/characters/:id’) that shows the Wikipedia abstract of a single character. Let’s follow along on what happens when we make a request for a specific character :

First stop : React-Router

The first place we hit in our flow is src/app.server.js#renderApp. We call matchRoutes — which is just React-Router’s match method wrapped in a promise (callbacks are so 2014). We let React-Router do it’s magic — it will find the components that need to be rendered according to the route. We’ll get back a props object, which has the info on what’s about to be rendered.

Setting up the app on the server

The props object go to the initAndRender method. Let’s break that down :

function* initAndRender(props) {const appStore = yield initStore(props);const initialState = serializeStore(appStore);const app = shared.injectStores(<RouterContext {...props} />, appStore);const componentHTML = renderToString(app);return renderIndex(componentHTML, initialState);}

initStore(props)

function* initStore(props) {const appStore = new AppStore();

yield props.components.filter(Boolean).map(component => component.wrappedComponent || component).filter(component => component.preServerRender).map(component => component.preServerRender(appStore, props.params));

return appStore;}

This is called with yield since — as the name of this post implies — it will be an asynchronous action. This will create a new AppStore object — the store is empty at this stage. Then we’ll go over the components in the props object. These are the components that React-Router found that need to be rendered (They are the classes of the components and not instances). We’ll be calling the static method preServerRender on each component (This is something I made up — not a built in React Component life-cycle method). This is the place for each component to say what it needs before it’s rendered on the server. Let’s look at it on the Character component :

static preServerRender(appStore, params) {return appStore.loadCharacter(params.pageId);}

So the Character component will need our appStore to load a character before it renders. We also get the route params, so we know which character ID to use. This method needs to return a Promise, so we’ll know when it’s finished loading the resource. Behind the scenes, this will preform a GET request to Wikipedia’s API.

Synchronous from here on

That’s it ! That was the whole asynchronous stuff. I’ll cover the rest pretty quick, but it’s ‘standard’ React server side rendering :

const initialState = serializeStore(appStore);

Will serialize the state. In order for our client-side application to start with the state our server finished with, it needs to be serialized into JSON and planted in the response the server sends. That happens here. Next we render the views :

const app = shared.injectStores(<RouterContext {...props} />, appStore);const componentHTML = renderToString(app);

Sending the state to the client-side application is not enough. We also want our initial views (HTML) to be rendered according to this state. s_hared.injectStores_ is just some code shared between the client and the server, utilizing Mobx’s Providers to inject the appStore into our Component instances. renderToString is just React-DOM doing it’s thing.

return renderIndex(componentHTML, initialState);

Now we take our rendered views (HTML) and serialized state (JSON), and inject those into our index.html. If you have a bigger index.html file, I’d recommend using ejs or jade for this part.

Don’t forget the client

After our initial render, the client-side application takes over. We’ll need to initialize our appStore on the client with the initial state — this happens in src/app.client.js :

const initialState = global.window.__INITIAL_STATE__;const appStore = new AppStore(initialState);

React will know to take it from there (That’s actually amazing). You’ll even get a warning in your console if the state and HTML you got from the server don’t align.

Client from here on

The rest of the interaction our user does with the app, will be done on the client-side. The Character component will use the componentDidMount React life-cycle hook to get the next characters :

componentDidMount() {const { appStore, params } = this.props;appStore.loadCharacter(params.pageId);}

This happens in parallel to the renders, so don’t forget to support loading states in your components.

That’s it. I didn’t really go very deep into things, I tried to keep this post short. So if you have any questions please post them here as comments. Thanks !

Edit : This will work for nested routes, but not for components nested inside components. That’s because those only come into play in when Component.render is called. That can be handled by rendering twice as described in react-async-render.


Published by HackerNoon on 2016/10/11