logo
Navigate back to the homepage

Server Side Rendering, the better way!

Kamlesh Chandnani
September 22nd, 2020 · 8 min read

Setting up SSR(Server Side Rendering) with React has always been a nightmare for most of us. But trust me after reading this you will realise that it’s not as difficult as it looks at face. In this post I’ll be demonstrating what are different types of rendering and then we’ll see how you can setup SSR in your new/existing projects from scratch once and for all.

So to begin with, what is SSR?

Before answering that question let’s go one step back What is rendering?

What is Rendering?

In layman terms rendering is a process which puts some pixels on your screen and create something visual with which your users can interact with.

What are different types of rendering?


1. Client Side Rendering(CSR)


This is the simplest and straightforward way of rendering that we do usually knowingly/unknowingly. If you’re using Create React App for creating your react projects you’re doing CSR.

Let’s see how does it looks like

release

What happens here is that when a client requests your application by entering a URL in the browser the server returns a basic markup with script tags which is the bundle of your application (since now your JS is responsible for rendering the whole app) so the browser now needs to download, parse and execute this JavaScript before anything is shown to the user. Hence we show loaders until all this is happening. Below is a sample Markup that your client receives in a typical CSR model.

1<html>
2<head>
3 <title>My App</title>
4</head>
5<body>
6 <div id="root"></div>
7<body>
8<script type="text/javascript" src="https://app.com/js/main-123xcr.js"></script>
9</html>

There is no harm in CSR if your product is not concerned with SEO because SEO bots depend purely on semantic HTML and what your users can see before even downloading a single byte of JavaScript and there are some performance gains as well that are attributed to Server Side Rendering

P.S. There are rumors that the SEO bots today are able to parse pure JavaScript applications as well but still the initial HTML markup without downloading a single byte of JavaScript gets preferred and is ranked higher.

2. Server Side Rendering(SSR)


In a typical server sider rendering model we are dependant on a server to generate a basic HTML which has some meaning and then the client will take it up from there and finish the remaining process of attaching the event listeners and making the page interactive for our users. This process is also known as hydration.

This rendering model is also known has hybrid rendering where the server does partial things and the client(browser) takes up from where the server left and does the remaining stuff.

Let’s see how does it looks like

release

In the above picture the enhanced Html that the server sends looks something like this

1<!DOCTYPE html>
2<html>
3
4<head>
5 <style data-styled="true" data-styled-version="5.1.1">
6 .gYRKyY {
7 padding-left: 500px;
8 padding-right: 500px;
9 padding-top: 240px;
10 }
11
12 /*!sc*/
13 @import url('https://rsms.me/inter/inter.css');
14
15 /*!sc*/
16 body {
17 margin: 0;
18 }
19
20 /*!sc*/
21 * {
22 -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
23 -webkit-tap-highlight-color: transparent;
24 }
25
26 /*!sc*/
27 html {
28 font-family: 'Inter', sans-serif;
29 }
30
31 /*!sc*/
32 @supports (font-variation-settings:normal) {
33 html {
34 font-family: 'Inter var', sans-serif;
35 }
36 }
37 </style>
38 <title>Resplash</title>
39</head>
40
41<body>
42 <div id="app">
43 <div>
44 <h1 data-component="Heading" class="css-17aexfk-Heading">Resplash</h1>
45 <span data-component="Text" class="css-14krc0n-Text">A gallery of amazing photos</span>
46 </div>
47 <input type="text" placeholder="Search photos with any keyword" data-component="Input" class="css-1t5g2mc-Input" />
48 </div>
49 </div>
50</body>
51<script src="http://localhost:9000/example/build/client/js/main.js" defer></script>
52</html>

If you look closely the server has actually sent the initial set of styles and the components that can be rendered directly by the browser and then the browser can parallelly download the JavaScript to make the page interactive. So our users essentially can get the visual feedback pretty quickly with SSR.

So all this sounds cool, but how do we create our own SSR setup with React from scratch?

Let’s begin! 🚀

We’ll be building Resplash which is similar to unsplash where you can search for images with any random keyword 😉. I’ll be using this GitHub repository for the demonstration purpose. Here’s a sneak peak of what we’ll be building

Home Page home

Search Results Page search

SSR, the traditional way

To begin with the setup since we need a server to render something, we need to tell it how it should render and since we need a server we’ll be using an express server for this. React provides a function called renderToString in react-dom/server package that we need to use on the server. As the name suggests this method will take your React tree as an input and return the actual Html markup which we can then send to the browser.

Also now since the server is in the picture which means we need to do the rendering twice once on the server and then on the client, oh and what about the data our components need initially 🤔. Yes this is where it starts getting confusing. But relax, we’ll see step by step how we can achieve this.

1. Component Initial Data


First of all we need to identify if the component that gets rendered by the route needs initial data, if yes we need to fetch that data and send it in our Html markup from server. Let’s see how to do that

To begin with we need to convert our declarative routes to imperative route config because we need to refer to our routes on the client as well as on the server.

This is how our routing looks like as of now

1// Wrapper.js
2const Wrapper = () => (
3 <ThemeProvider tokens={tokens} components={components}>
4 <GlobalStyles />
5 <Switch>
6 <Route exact path="/">
7 <Home />
8 </Route>
9 <Route exact path="/photos">
10 <Listing />
11 </Route>
12 </Switch>
13 </ThemeProvider>
14);

We’ll make some changes to the above code so first of all let’s create a file routes.js with the following content

1const routes = [
2 {
3 path: '/',
4 exact: true,
5 Component: Home,
6 },
7 {
8 path: '/photos',
9 exact: true,
10 Component: Listing,
11 },
12];

Now, let’s modify our Wrapper.js to accomodate the above change.

1import routes from '../routes/routes';
2
3const Wrapper = () => (
4 <ThemeProvider tokens={tokens} components={components}>
5 <GlobalStyles />
6 <Switch>
7 {routes.map(({ path, exact, Component }) => (
8 <Route key={path} path={path} exact={exact}>
9 <Component />
10 </Route>
11 ))}
12 </Switch>
13 </ThemeProvider>
14);

The next step is to define the function that will fetch the data for our component on the server and pass the data to our component. In our case we need to do that in the Listing’s page. We’ll define a function called getInitialData on the Listing component.

1// Listing.js
2const Listing = () => {
3 // rendering logic
4}
5
6// Function that we'll call on server to fetch the initial data
7Listing.getInitialData = async (req) => {
8 const { keyword } = req.query;
9 const response = await getPhotos(keyword);
10 return { photos: response.results };
11};

In the next step we’ll see how do we call this function on the server and pass the data to our component.

2. Server Configuration


First of all create a file called server.js in your project which will create an instance of express server and we’ll be exposing a path on which our app will get rendered. Here’s how it will look like

1import serialize from 'serialize-javascript';
2
3app.get('*', (req, res) => {
4 const activeRoute = routes.find((route) => matchPath(req.path, route)) || {};
5 let initialData = {};
6
7 if (activeRoute?.Component?.getInitialData) {
8 initialData = await activeRoute.Component.getInitialData(req);
9 }
10
11 const sheet = new ServerStyleSheet();
12
13 // get the html markup of our application
14 const markup = renderToString(
15 <StyleSheetManager sheet={sheet.instance}>
16 <StaticRouter location={req.url}>
17 <Wrapper /> {/* Our actual application */}
18 </StaticRouter>
19 </StyleSheetManager>,
20 );
21 const styles = sheet.getStyleTags();
22
23 // send the html markup in response
24 res.send(`
25 <!DOCTYPE html>
26 <html>
27 <head>
28 ${styles}
29 <title>Resplash</title>
30 <script>window.__INITIAL_DATA__=${serialize(initialData)}</script>
31 </head>
32 <body>
33 <div id="app">${markup}</div>
34 </body>
35 <script src="${configJs.assetsUrl}/${packageJson.name}/build/client/js/main.js" defer></script>
36 </html>
37`);
38});

The source of entire server.js can be found here

Here’s what we are doing in a nutshell:

  1. For the current requested route we check if the component has getInitialData function defined and call it to get the initial data for that route. (Line #8)
  2. Extract the styles from our application - We are using styled-components which provides StyleSheetManager to do this job. (Line #15)
  3. Match the route and persist it across client and server - We are using React Router which provides the StaticRouter component for this. (Line #16)
  4. Render our actual application. (Line #17)
  5. Get the style tags from Styled Components for that route. (Line #21)
  6. Attach the initial data that we fetched for that route in a global window object. (Line #30)
  7. Send the Html markup by attaching style tags and our markup from the steps above. (Line #24)

After above things are done open the Listing.js to handle the initial data that was sent by our server

1// Listing.js
2const Listing = () => {
3 const [fetching, setFetching] = React.useState(false);
4 const [photos, setPhotos] = React.useState([]);
5 const search = new URLSearchParams(useLocation().search).get('keyword');
6
7 const fetchPhotos = async (keyword) => {
8 setFetching(true);
9 const response = await getPhotos(keyword);
10 setFetching(false);
11 setPhotos(response.results);
12 };
13
14 React.useEffect(() => {
15 if (window?.__INITIAL_DATA__?.photos) {
16 // render photos - the page was rendered on the server and server sent the data
17 setPhotos(window.__INITIAL_DATA__.photos);
18 } else {
19 // make the api call on client - the page was navigated from client side
20 fetchPhotos(search);
21 }
22 }, [search]);
23
24 return (
25 // render photos
26 );
27};

So what we are essentially doing here is when the page gets rendered on the server(direct URL request, or browser reload) we will load the data from window.__INITIAL_DATA__.photos else if the page is navigated from the client side i.e from Home page(typical SPA - single page application) we won’t have initial data from server, hence we will make the api call from the client side.

Now, let’s see the client configuration

3. Client Configuration


We’ll create a file called client.js and add the following code

1ReactDOM.hydrate(
2 <BrowserRouter>
3 <Wrapper />
4 </BrowserRouter>,
5 document.getElementById('app'),
6);

The source of entire client.js can be found here

Let’s see what’s going on here. We used a method called ReactDOM.hydrate from react-dom. If you can recall then typically(when doing CSR) we use ReactDOM.render method but when we are doing SSR we use renderToString on the server so this method does some amount of work on the server and hence in order to sync the remaining work on the client(browser) side we have to use ReactDOM.hydrate which basically means hydrate the Html markup returned from the server and don’t start everything from scratch.

So we are able to get the basic SSR working. But there are few issues with this approach.

  • Duplication: If you see we are maintaining 2 files now client.js and server.js and it has a lot of duplicate code. For example we need to write a router on both the client as well as the server. If we add more providers, for example, Redux, Apollo etc someone has to keep going to both the server and client files and add it at both the places so they remain in sync.
  • Maintenance: It’s risky and adds a lot more overhead because once your application grows, your team grows you basically lose control on all this and if there’s a mismatch in your client and server React tree then react will just throw all the work that was done on the server and instead of hydrating it will just restart all the efforts.
  • Performance: This was the main reason why we opted for SSR but if there is a mismatch between the client and the server then the performance goes for a toss since all the work that the server did will now be discarded and react will boot the whole application from scratch which makes it similar to CSR now.

You might be familiar with this warning when the server and client content mismatch

warning

So how shall we address the above problems? 🤔

SSR, the better way

Before we jump to the solution let’s identify what do we expect from the solution?

  1. A nice abstraction - As a developer I’m only concerned about what my application is meant to do. How and where to render it should be abstracted out.
  2. Central configuration - I want to have one place which holds all my configuration, instead of multiple places in different files.
  3. Resilient Setup - Make SSR setup more resilient than fragile.

Now with the above goals in mind let’s see how we can abstract all these things with a nice API.

1. Abstraction


Let’s create a function that’ll abstract all the SSR things for us

1export const renderApp = ({ App, req, initialData = {} }) => {
2 if (__IS_BROWSER__) {
3 ReactDOM.hydrate(
4 <BrowserRouter>
5 <App />
6 </BrowserRouter>,
7 document.getElementById('app'),
8 );
9 } else {
10 const sheet = new ServerStyleSheet();
11 const markup = renderToString(
12 <StyleSheetManager sheet={sheet.instance}>
13 <StaticRouter location={req.url}>
14 <App />
15 </StaticRouter>
16 </StyleSheetManager>,
17 );
18 const styles = sheet.getStyleTags();
19 const html = `
20 <!DOCTYPE html>
21 <html>
22 <head>
23 ${styles}
24 <title>Resplash</title>
25 <script>window.__INITIAL_DATA__=${serialize(initialData)}</script>
26 </head>
27 <body>
28 <div id="app">${markup}</div>
29 </body>
30 <script src="${__CONFIG__.assetsUrl}/${__NAME__}/build/client/js/main.js" defer></script>
31 </html>
32 `;
33 return html;
34 }
35};

The complete source code can be found here

Let’s see what’s happening here:

  1. We have clubbed our client.js and server.js into one function.
  2. The function accepts the following as input
    • The main App that needs to be rendered
    • The req object to handle the server things
    • The initial data that we might send for a particular route.
  3. We have one flag __IS_BROWSER__(we’ll talk about this in a while) which decides whether this function is being called from client or server. But the developers are completely unaware about this.

Now, let’s see the usage of this function

2. Client configuration


open the client.js we created earlier

1// old client.js
2ReactDOM.hydrate(
3 <BrowserRouter>
4 <Wrapper />
5 </BrowserRouter>,
6 document.getElementById('app'),
7);

Now replace the above code with the following snippet

1// updated client.js
2import { renderApp } from 're-ssr/renderApp';
3
4renderApp({ App: Wrapper });

Open webpack.client.js and add this to the list of plugins

1plugins: [
2 new webpack.DefinePlugin({
3 __IS_BROWSER__: 'true',
4 }),
5]

3. Server configuration


open the server.js we created earlier

1// old server.js
2app.get('*', (req, res) => {
3 const sheet = new ServerStyleSheet();
4
5 // get the html markup of our application
6 const markup = renderToString(
7 <StyleSheetManager sheet={sheet.instance}>
8 <StaticRouter location={req.url}>
9 <Wrapper /> {/* Our actual application */}
10 </StaticRouter>
11 </StyleSheetManager>,
12 );
13 const styles = sheet.getStyleTags();
14
15 // send the html markup in response
16 res.send(`
17 <!DOCTYPE html>
18 <html>
19 <head>
20 ${styles}
21 <title>Resplash</title>
22 </head>
23 <body>
24 <div id="app">${markup}</div>
25 </body>
26 <script src="${configJs.assetsUrl}/${packageJson.name}/build/client/js/main.js" defer></script>
27 </html>
28`);
29});

Now replace the above code with the following snippet

1// updated client.js
2import { renderApp } from 're-ssr/renderApp';
3
4app.get('*', (req, res) => {
5 const activeRoute = routes.find((route) => matchPath(req.path, route)) || {};
6 let initialData = {};
7
8 if (activeRoute?.Component?.getInitialData) {
9 initialData = await activeRoute.Component.getInitialData(req);
10 }
11
12 const html = renderApp({ App: Wrapper, req, initialData });
13 res.send(html);
14});

Open webpack.server.js and add this to the list of plugins

1plugins: [
2 new webpack.DefinePlugin({
3 __IS_BROWSER__: 'false',
4 }),
5]

Let’s see what we did in a nutshell:

  1. Just call one function renderApp inside client.js as well as server.js and pass your App.
  2. Use webpack define plugin to identify the bundle will run on browser or server at compile time only(this also helps reduce bundlesize) which will help the renderApp function to either return the server markup or return the hydrated tree for client. Read more about DefinePlugin.
  3. Removed so many lines of code.

Hoorah! we have configured the SSR from scratch the easier and the better way 🎉

Wrap up 📝

Sometimes there are a lot of simple things that we end up doing repetitively and then later get fed up with it which makes things worse and fragile. But that’s also the time when we can look deeper and see if there exists a potential of abstraction. The above abstraction actually originated when I was working at Treebo and whenever we used to refactor or upgrade our existing packages or add any new packages we had to update the React tree and we often used to struggle to keep the changes on both the client and server in sync but it used to get missed somehow becauase again it was a manual tedious process and then our performance used to get affected. So we went deeper and discovered that there exists a potential and hence the above solution was born. After this we never had any sync issues, we removed a lot of redundant code and the maintenance went down to 0 and we used to ship our SSR apps with confidence 💪🏻

Interestingly, we abstracted out this as a common package and then it helped us to move our existing and upcoming projects to SSR with breeze and without any hiccups 🚀


If you have run into problems with SSR and solved them with different solutions I would love to hear your stories. You can write it to me or you can DM me on Twitter.

If you like this then don’t forget to
🐤 Share
🔔 Subscribe
➡️ Follow me on Twitter

Join the Newsletter

Subscribe to get the latest write-ups about my learnings from JavaScript, React, Design Systems and Life

No spam, pinky promise!

More articles from Kamlesh Chandnani

Mentoring - A natural process

You don't need to wear a title for mentoring and giving feedbacks

July 28th, 2020 · 3 min read

Code and Detachment

Sometimes detachments are necessary to boost productivity

July 14th, 2020 · 3 min read
Link to https://github.com/kamleshchandnaniLink to https://twitter.com/@_kamlesh_Link to https://www.youtube.com/playlist?list=PLpATFO7gaFGgwZRziAoScNoAUyyR_irFMLink to https://linkedin.com/in/kamleshchandnani