2. Better SEO, because the crawlers can better index the page. Even though, search engine crawlers are catching up to the frontend frameworks.
Hence, it is very common for developers to opt for server-side rendering. Solutions like Next.js, Gatsby.js, Razzle, etc. help us get started with server-side rendering. But, they come with their own opinionated choices that one might disagree with or may choose to create their own solution for any given reason. Therefore, we will implement a basic Server-side rendered React app to understand the fundamental concepts about it.
Note: These article assumes the user has experience working with React and Webpack. If not, you may like go over our other articles - Introduction to React, Introduction to Webpack 4.
$ npm init -y
Then, we will install the dependencies we will need
$ npm i react react-dom react-router-dom react-helmet express cors axios
We will be using
react-helmet in our react app. We will need
express to build the server that will be responsible for server rendering the app. We will need
cors to allow Cross Site Requesting from the API server and
axios for a unified way to query the API on both client and server. You can also use something like
isomorphic-fetch instead of
Next, we will get our dev dependencies
$ npm i -D webpack webpack-cli webpack-node-externals nodemon babel-core babel-loader babel-preset-env babel-preset-react babel-preset-stage-2
We will need
babel-loader and all its presets for transpiling our code. We will need
webpack for bundling while
webpack-cli will be watching the filesystem and rebundling if anything changes which will in turn trigger
nodemon to restart our server. We will not be setting up anything fancy like hot module replacement (HMR), etc. so that we can focus solely on the bare bones necessities of server-side rendering. Lastly,
webpack-node-externals will help us exclude externals modules from our server bundle like
react because we will generate markup on the server and not run a full react app.
Setup Babel and Webpack
We will create two webpack configs, one for the client and the other for the server. We will import the packages we need and extract the common stuff like the js module bundling using
babel-loader in a separate variable. Then, we will create two separate variables holding the client and server config separately.
For the client, we have entry file at ./src/client/index.js and output at ./public/bundle.js. And for the server, we have entry file at ./src/server/index.js and output at ./build/server.js. One thing to notice is in both the config we are using webpack.DefinePlugin to create a global constant at the compile time. It will be used in our react components to check whether we are rendering on the server or the client and fetch the initial data accordingly. One last thing to talk about here is the
externals option in the server config. We will use the
webpack-node-externals package here to exclude external packages as mentioned earlier.
Now, we move onto our babel configuration
We will use the env preset with specific browser and node targets to simplify our transpiling needs. We will also use the react preset because we will be using JSX and stage-2 preset because we will be using features like Object spread and rest.
Lastly, we create the directory structure as per the webpack config, i.e. ./src/client and ./src/server to hold respective source files. We also create a common shared directory under src. It will hold all the components that will be accessed in both the cases.
Now, let's dive into the code. We will start with the easy and familiar part, i.e. the presentational components (components that do not hold any business logic but only presentational UI related code).
Home component accepts
data props and renders either a
Loading component or a
Table with a collection of movie title and rating. Each of the movie titles also link to their respective movie page i.e the
Movie component. The
Movie component also accepts the same props and renders a card UI with the movie title, rating and plot description. It also renders a button that links back to the home page. Both of them utilize the
Helmet component from
react-helmet to manipulate the
<title> tag based on the currently active route.
Navbar component renders a link to the home page as well as the app title.
Loading component renders a loader while
NoMatch is the 404 destination.
Next, we will look at the
UniversalDataloader (named so because it is responsible for loading the necessary data on both the server and the client) component that provides the
data props to the
Movie component. But let's first encapsulate all the necessary API fetch requests so that it's easier for us to understand the data retrieving process.
fetchAllMovies provides all movie listing and is hence used for populating the
Home component and
fetchMovieById is used for the
This component utilizes the render props pattern to pass down necessary props to both the
Home and the
Movie component. The constructor is responsible for populating the initial data. It first checks whether it is the browser or the server. If it is the browser, it checks and fetches the data from
window.__SERIALIZED_DATA__ which will be set on the server side (We will get back to this when we discuss server-side code). If it is the server, then we get the data from
this.props.staticContext that is also set during server-side render using
StaticRouter component from
react-router-dom. Finally, we
setState with the necessary data.
componentDidMount is used to handle cases where the user navigates between the home and a movie route on the client-side.
this.props.match.params.id is provided by the
react-router-dom based on the route that is currently rendered to fetch the correct movie's data. In case of the home route, the id will be
undefined while in case of a movie route it will hold the corresponding movie id. The
fetchData method just wraps the
getInitialData method passed to the component via props for API request and also handles state update efficiently.
Finally, the render props pattern is used to pass down its state as props to the child component.
Next, we will setup all the routing logic for the application. One thing to note here is, we cannot declaratively implement the routes here like we usually do with client-side apps because our server needs to be aware of the routing logic as well. We will achieve this by storing our route related logic in a way accessible by both the server and the client.
We list all the routes and the components that need to be rendered accordingly in this file. One thing to note is the custom
getInitialData props, this method abstracts away the specific API call needed to fetch the initial data. This is what enables our
UniversalDataloader to function regardless of what component is being rendered or what route it is.
We will now wrap all the constituents up in our
In this, we first render the
Navbar component, then we map over all the
routes from routes.js and render specific
Route components wrapped under
Switch component from
react-router-dom. We also render a fallback
NoMatch component for invalid routes. One thing to note is that we use nested render props to render the route specific component with data. We nest the
Route render props with our own
UniversalDataloader render props (flashback callback hell).
We finish up our client-side code by wrapping the
App component under
react-router-dom. We also use
ReactDOM.hydrate instead of
ReactDOM.render here. This is so that client-side react can pick up from where the server left off. The server generates only the markup and the client-side uses that existing markup to attach only the event handlers to make the app interactive instead of rendering everything from scratch. This makes the process performant. This is also why server-side markup and client-side markup must always match up.
Last but definitely not the least, let's dive into the server rendering code.
First, we initialize an
express app and setup
cors so that we can initiate Cross-Site Requests to our API. We also tell
express to serve static assets from the public directory. Then, we create a middleware function to intercept all traffic to the server and respond with the generated markup.
The middleware is responsible for generating the markup. Inside the middleware, we first determine the active route by using
matchPath function from
react-router-dom. This is the very function used internally by react-router to determine the correct active route. This ensures there can be no route resolution mismatch between the client and the server. Then, we use the
getIntialData method on the
activeRoute object to fetch the data necessary to populate the components. Upon response from the API, we use
ReactDOM.renderToString to generate the markup of our react app and also use
Helmet.renderStatic to render the
<title> tag markup (this is important, otherwise it will throw an error). One thing to note is that we wrap our
StaticRouter here, because
BrowserRouter cannot be used on the server as it depends on certain browser APIs. We also pass
location props with
req.url to it. This is to maintain the client and server side markup match. We pass
data onto the
context props, because if you remember we use
this.props.staticContext.data to get data on the server for our
Once, we have all the necessary information we pass
title to the
template function which spits out the complete HTML structure that is served as the server response. We set up the stringified
data value on
window.__SERIALIZED_DATA__ for use in the
Movie components when rendering on the client.
Server-side rendering is a very powerful tool and must be used wisely. It is not a "cure-all" solution to performance problems in React apps. It has its downsides too. It can slow down an application if the markup needed to be generated on the server is heavy or the server workload is excessive. It is a tradeoff like any other implementation decision.
This tutorial is by no means a production-ready solution but it is a good starting point to understand how server-side rendering works. Feel free to fork and play around with the source code - Github