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
await store.dispatch(fetchGeneral());
const initialRender = renderToString(
<RenderServerside store={store} location={url} />
);
const initialState = store.getState();
With these steps we can preload and parse the state to the client. But what do we need to preload for this page?
Let’s break the complexity down to a few challenges
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.
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.
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?
Breaking down the complexity into components
class App extends Component {
componentDidMount() {
const { name } = this.props;
if (name) return;
this.props.fetchGeneral();
}
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:
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.
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.
class App extends Component {
static preInitStore() {
this.props.fetchGeneral();
}
Resolving the restrictions of a static method
await App.preInitStore(store);
return {
renderedString: renderToString(...
class App extends Component {
static preInitStore(store) {
store.dispatch(fetchGeneral());
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.)
Let’s wait for a response
class App extends Component {
static async preInitStore(store) {
await store.dispatch(fetchGeneral());
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.
Read more about async, await and promises in Javascript from the Mozilla documentation.
Tackling all our challenges!
class App extends Component {
static async preInitStore(store) {
await store.dispatch(fetchGeneral());
await Routing.preInitStore(store);
}
export class Routing extends Component {
static async preInitStore(store) {
const state = store.getState();
await store.dispatch(fetchRoutingData(state.route));
(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.)
Just one more optimization awaits!
module.exports = {
...
module: {
rules: [{
test: /.js?$/,
use: [{
loader: 'webpack-strip-block',
options: {
start: 'SERVERSIDE-ONLY:START',
end: 'SERVERSIDE-ONLY:END'
}
}]
}]
}
...
}
class App extends Component {
/* SERVERSIDE-ONLY:START */
static async preInitStore(store) {
...
}
/* SERVERSIDE-ONLY:END */
Conclusion
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.
Top comments (0)