How to load the correct data on the server-side with respect to nested components, with react-redux and react-routerDoor Wessel Kroos
The moment you dip your toes in the world of server-side rendering things can get complicated quickly. Especially in large applications which contain a lot of nested components and api calls, and each of them called and rendered in the browser only when it’s required. We sure want to preload the data that’s required to show you the header on this website. But do I always need to preload the data that’s on our homepage? You might have found this blog post on Google.com and may never visit our homepage or all our other blogposts today. And what about a nested component in this article, under which conditions do we preload it’s data? Let’s answer those questions.
Initial project setup
While tackling this problem we are going to use the express package as our webserver and use React’s renderToString() method to render all the components on the server-side. server.js: Fetch the data and render the webpage.
render.js: Serialize the initial state and parse it through the window.initialState object in the script tag to our clients.
Server-side rendering in React step-by-step
With these steps we can preload and parse the state to the client. But what do we need to preload for this page?
1. Currently we execute only one fetch before we start rendering the page on the server-side, but we also have multiple nested components on our website. This expands the code in this file with multiple if statements to decide which data we need to fetch. This will make the code unmaintainable, therefore we are better off when we let the components decide that for themselves.
2. Without server-side rendering you fetch data on the client-side in the componentDidMount() method. With server-side rendering you use renderToString() to render the components. But the renderToString() method does not attach the rendered components to the DOM, so the componentDidMount() method is never called on the server-side. We need another way to make the code in the componentDidMount() method available to the server-side.
3. You might have a nested component which depends on data from a parent component. How do we wait for responses in our parent component and parse the data to our child components?
A perfect place to decide which data we need and fetch the data on the client-side is the componentDidMount() method. This way we can start fetching right away when the component has been mounted or skip fetching if the data is already available in the store.Fetching data in the componentDidMount method
When we copy this logic to the server-side we duplicate logic into two separate parts of the application. The component and the server-side renderer function. Even more problematic, we bundle logic from all components into one function and make on file unnecessarily complex. Every component has its own set of rules whether to render a child component, so this function will grow immensely in the future. It’s almost impossible for a developer to determine in that single function what data is required in all our nested components and maintain it in the future. And when a new developer joins the team there’s a big chance he or she will probably edit a component but forget to update our decision tree on the server-side as well. We don’t want that to happen. So let’s tackle challenge number 1 and move this complexity away from the server.js file into the components itself by keeping this logic in the componentDidMount() method.
There are just two problems:
1. The didComponentMount() method is never called when we use React’s renderToString() function. So we need to call the didComponentMount() method from the server-side ourselves.
2. We need to call this method before we execute renderToString() because the renderToString() function needs a store with prefetched data. Since we have no constructed React components in this stage we need to make the method in our React components static.
So let’s tackle challenge number 2 and make this method available from the server-side. We do this by moving the code into a new static method called preInitStore(). This way we can execute it with the code App.preInitStore() from the server-side.
We can now call the App.preInitStore() method before executing renderToString(). But since the preInitStore() method is static we also have no reference to the App component in the this property and thus cannot call the this.props.fetchGeneral() method. Luckily there is a way to dispatch an action from the store object with the store.dispatch() method. So we need to parse the store from the server-side into the preInitStore() method as a parameter:Passing the store to the App component
…now we can execute it in our preInitStore() method:Dispatching the fetchGeneral action creator ourself in the static preInitStore() method
Now we have a method that we can call from the server-side while all the logic resides in the component itself.
(Note: Because we now have a static method in our component we can also share other static methods between the server-side and client-side code inside the component.)
An important part of our solution is still missing. Since the fetch calls in our actions are promises the browser needs to wait for those promises to be resolved before we can execute the renderToString() method. A way we can facilitate that is by awaiting those promises in our preInitStore() method and also in the action creators.Awaiting the fetch in the action creator
Awaiting the fetch in the preInitStore() method
With this modification the caller of the App.preInitStore() method can wait untill the data is received from the API and saved into the store.
And now it’s time combine the pieces of the puzzle so we can tackle challenge number 3! When we await all dispatch() methods in child components as well the App component can now await the preInitStore() method in child components.Waiting for fetches in child components by using the await keyword
And since we await the fetchGeneral() action in the App component before we execute the preInitStore() method of child components we tackled challenge number 3 as well! Because child components can get that data by using the store.getState() method.
Getting the new state from the store after the parent component fetched and saved its data in the store.
(Tip: The App’s preInitStore() method is now in charge of calling preInitStore() methods of child components. So in case of react-router this would be an ideal location to decide which component to initialize by checking the URL from the express webserver. See the full GitHub project for an example.)
We have now moved server-side code into components. But the preInitStore() method is never used on the client-side. We can optimize this so that we can spare some bytes for our visitors by using the webpack-strip-block webpack plugin. Let’s configure this plugin in a way that it removes any code marked as SERVERSIDE-ONLY so that it’s stripped away from our final client bundle.Install webpack-strip-block
And add webpack-strip-block to our webpack.config.js
Now we can exclude our preInitStore() methods from the client bundle by adding 2 comments:
Excluding serverside code from the client bundle
We were able to reduce the complexity of our server-side rendering function and make our code maintainable:
- By splitting server-side state fetching logic back into the components.
- By adding the static async preInitStore() method and make it available from the server.
- And by using async / await in the preInitStore() method and actions. So that we can wait for API responses and use the data that has been fetched by a parent component in the child components.
I hope I was able to make your server-side rendered websites more maintainable. If you have questions or want to try it for yourself you can take a look at the complete solution on GitHub via the link below. There is a react-router example in it as well.