Server-Side Rendering for Fun and Profit

The next step in building React applications is learning how server-side rendering works and what benefits it can give us. The universal applications are better for SEO, and they enable knowledge-sharing between the frontend and the backend. They can also improve the perceived speed of a web application, which usually leads to increased conversions. However, applying server-side rendering to a React application comes at a cost, and we should think carefully about whether we need it or not.

In this chapter, you will see how to set up a server-side rendered application, and by the end of the relevant sections, you will be able to build a universal application and understand the pros and the cons of the technique.

In this chapter, we will cover the following topics:

  • Understanding what a universal application is
  • Figuring out the reasons why we may want to enable server-side rendering
  • Creating a simple static server-side rendered application with React
  • Adding data fetching to server-side rendering and understanding concepts such as dehydration/hydration
  • Using Next.js by Zeith to easily create a React application that runs on both the server and the client

Technical requirements

To complete this chapter, you will require the following:

  • Node.js 12+
  • Visual Studio Code

You can find the code for this chapter in the book's GitHub repository at https://github.com/PacktPublishing/React-17-Design-Patterns-and-Best-Practices-Third-Edition/tree/main/Chapter09.

Understanding universal applications

A universal application is an application that can run both on the server side and client side with the same code. In this section, we will look at the reasons why we should consider making our applications universal, and we will learn how React components can be easily rendered on the server side.

When we talk about JavaScript web applications, we usually think of client-side code that lives in the browser. The way they usually work is that the server returns an empty HTML page with a script tag to load the application. When the application is ready, it manipulates the DOM inside the browser to show the UI and to interact with users. This has been the case for the last few years, and it is still the way to go for a huge number of applications.

In this book, we have seen how easy it is to create applications using React components and how they work within the browser. What we have not seen yet is how React can render the same components on the server, giving us a powerful feature called Server-Side Rendering (SSR).

Before going into the details, let's try to understand what it means to create applications that render both on the server and the client. For years, we used to have completely different applications for the server and client: for example, a Django application to render the views on the server, and some JavaScript frameworks, such as Backbone or jQuery, on the client. Those separate apps usually had to be maintained by two teams of developers with different skill sets. If you needed to share data between the server-side rendered pages and the client-side application, you could inject some variables inside a script tag. Using two different languages and platforms, there was no way to share common information, such as models or views, between the different sides of the application.

Since Node.js was released in 2009, JavaScript has gained a lot of attention and popularity on the server side as well, thanks to web application frameworks, such as Express. Using the same language on both sides not only makes it easy for developers to reuse their knowledge, but also enables different ways of sharing code between the server and the client.

With React in particular, the concept of isomorphic web applications became very popular within the JavaScript community. Writing an isomorphic application means building an application that looks the same on the server and the client. The fact that the same language is used to write the two applications means that a big part of the logic can be shared, which opens many possibilities. This makes the code base easier to reason about and avoids unnecessary duplication.

React brings the concept a step forward, giving us a simple API to render our components on the server and transparently applying all the logic needed to make the page interactive (for example, event handlers) on the browser.

The term isomorphic does not fit in this scenario because, in the case of React, the applications are the same, and that is why one of the creators of React Router, Michael Jackson, proposed a more meaningful name for this pattern: Universal.

Reasons for implementing SSR

SSR is a great feature, but we should not jump into it just for the sake of it. We should have a real and solid reason to start using it. In this section, we will look at how SSR can help our application and what problems it can solve for us. In our next sections, we are going to learn about SEO and how to improve the performance of our application.

Implementing search engine optimization

One of the main reasons why we may want to render our applications on the server side is Search Engine Optimization (SEO).

If we serve an empty HTML skeleton to the crawlers of the main search engines, they are not able to extract any meaningful information from it. Nowadays, Google seems to be able to run JavaScript, but there are some limitations, and SEO is often a critical aspect of our businesses.

For years, we used to write two applications: an SSR one for the crawlers, and another one to be used on the client side by users. We used to do that because SSR applications could not give us the level of interactivity users expect, while client-side applications did not get indexed by search engines.

Maintaining and supporting two applications is difficult, and makes the code base less flexible and less prone to changes. Luckily, with React, we can render our components on the server side and serve the content of our applications to the crawlers in such a way that it is easy for them to understand and index the content.

This is great, not only for SEO, but also for social sharing services. Platforms such as Facebook or Twitter give us a way of defining the content of the snippets that are shown when our pages are shared.

For example, using Open Graph, we can tell Facebook that, for a particular page, we want a certain image to be shown and a particular title to be used as the title of the post. It is almost impossible to do that using client-side-only applications because the engine that extracts the information from the pages uses the markup returned by the server.

If our server returns an empty HTML structure for all the URLs, the result is that when the pages are shared on the social networks, the snippets of our web application are empty as well, which affects their virality.

A common code base

We do not have many options on the client side; our applications have to be written in JavaScript. There are some languages that can be converted into JavaScript at build time, but the concept does not change. The ability to use the same language on the server represents a significant win regarding maintainability and knowledge-sharing across the company.

Being able to share the logic between the client and the server makes it easy to apply any changes on both sides without doing the work twice, which, most of the time, leads to fewer errors and fewer problems.

The effort of maintaining a single code base is less than the work required to keep two different applications up to date. Another reason why you might consider introducing JavaScript on the server side in your team is sharing knowledge between frontend and backend developers.

The ability to reuse the code on both sides makes collaboration easier, and the teams speak a common language, which helps with making faster decisions and changes.

Better performance

Last, but not least, we all love client-side applications, because they are fast and responsive, but there is a problem—the bundle has to be loaded and run before users can take any action on the application.

This might not be a problem using a modern laptop or a desktop computer on a fast internet connection. However, if we load a huge JavaScript bundle using a mobile device with a 3G connection, users have to wait for a little while before interacting with the application. This is not only bad for the UX in general, but it also affects conversions. It has been proven by the major e-commerce websites that a few milliseconds added to the page load can have an enormous impact on revenues.

For example, if we serve our application with an empty HTML page and a script tag on the server and we show a spinner to our users until they can click on anything, the perception of the speed of the website is significantly affected.

If we render our website on the server side instead and users start seeing some of the content as soon as they hit the page, they are more likely to stay, even if they have to wait the same amount of time before doing anything for real, because the client-side bundle has to be loaded regardless of the SSR.

This perceived performance is something we can improve greatly using SSR because we can output our components on the server and return some information to users straight away.

Don't underestimate the complexity

Even if React provides an easy API to render components on the server, creating a universal application has a cost. So, we should consider carefully before enabling it for one of the preceding reasons and check whether our team is ready to support and maintain a universal application.

As we will see in the coming sections, rendering components is not the only task that needs to be done to create server-side rendered applications. We have to set up and maintain a server with its routes and its logic, manage the server data flow, and so on. Potentially, we want to cache the content to serve the pages faster and carry out many other tasks that are required to maintain a fully functional universal application.

For this reason, my suggestion is to build the client-side version first, and only when the web application is fully working on the server should you think about improving the experience by enabling SSR. SSR should only be enabled when strictly necessary. For example, if you need SEO or if you need to customize the social sharing information, you should start thinking about it.

If you realize that your application takes a lot of time to load fully and you have already done all the optimization (refer to the following Chapter 10, Improving the Performance of Your Applications, for more on this topic), you can consider using SSR to offer a better experience to your users and improve the perceived speed. Now that we have learned what SSR is and the benefits of universal applications, let's jump into some basic examples of SSR in our next section.

Creating a basic example of SSR

We will now create a very simple server-side application to look at the steps that are needed to build a basic universal setup. It is going to be a minimal and simple setup on purpose because the goal here is to show how SSR works rather than providing a comprehensive solution or a boilerplate, even though you could use the example application as a starting point for a real-world application.

This section assumes that all the concepts regarding JavaScript build tools, such as webpack and its loaders, are clear, and it requires a little bit of knowledge of Node.js. As a JavaScript developer, it should be easy for you to follow this section, even if you have never seen a Node.js application before.

The application will consist of two parts:

  • On the server side, where we will use Express to create a basic web server and serve an HTML page with the server-side rendered React application
  • On the client side, where we will render the application, as usual, using react-dom

Both sides of the application will be transpiled with Babel and bundled with webpack before being run, which will let us use the full power of ES6 and the modules both on Node.js and on the browser.

Let's start by creating a new project folder (you can call it ssr-project) and running the following command to create a new package:

npm init

Once package.json is created, it is time to install the dependencies. We can start with webpack:

npm install webpack

After this is done, it is time to install ts-loader and the presets that we need to write an ES6 application using React and TSX:

npm install --save-dev @babel/core @babel/preset-env @babel/preset-react ts-loader typescript

We also have to install a dependency, which we will need in order to create the server bundle. webpack lets us define a set of externals, which are dependencies that we do not want to add to the bundle. When creating a build for the server, in fact, we do not want to add to the bundle of all the node packages that we use; we just want to bundle our server code. There's a package that helps with that, and we can simply apply it to the external entry in our webpack configuration to exclude all the modules:

npm install --save-dev webpack-node-externals

Great. It is now time to create an entry in the npm scripts section of package.json so that we can easily run the build command from the terminal:

"scripts": {
"build": "webpack"
}

Next, you need to create a .babelrc file in your root path:

{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}

We now have to create the configuration file, called webpack.config.js, to tell webpack how we want our files to be bundled.

Let's start importing the library we will use to set our node externals. We will also define the configuration for ts-loader, which we will use for both the client and the server:

const nodeExternals = require('webpack-node-externals')
const path = require('path')

const rules = [{
test: /.(tsx|ts)$/,
use: 'ts-loader',
exclude: /node_modules/
}]

In Chapter 8, Making Your Components Look Beautiful, we looked at how we had to export a configuration object from the configuration file. There is one cool feature in webpack that lets us export an array of configurations as well so that we can define both client and server configurations in the same place and use both in one go.

The client configuration shown in the following block should be very familiar:

const client = {
entry: './src/client.tsx',
output: {
path: path.resolve(__dirname, './dist/public'),
filename: 'bundle.js',
publicPath: '/'
},
module: {
rules
}
}

We are telling webpack that the source code of the client application is inside the src folder, and we want the output bundle to be generated in the dist folder.

We also set the module loaders using the previous object we created with ts-loader. The server configuration is slightly different; we need to define a different entry, output, and add some new nodes, such as target, externals, and resolve:

const server = {
entry: './src/server.ts',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'server.js',
publicPath: '/'
},
module: {
rules
},
target: 'node',
externals: [nodeExternals()],
resolve: {
extensions: [".ts", ".tsx", ".js", ".json"],
},
}

As you can see, entry, output, and module are the same, except for the filenames.

The new parameters are the target, where we specify the node to tell webpack to ignore all the built-in system packages of Node.js, such as fs and externals, where we use the library we imported earlier to tell webpack to ignore the dependencies.

Last, but not least, we have to export the configurations as an array:

module.exports = [client, server]

The configuration is done. We are now ready to write some code, and we will start with the React application, which we are more familiar with.

Let's create an src folder and an app.ts file inside it.

The app.ts file should have the following content:

const App = () => <div>Hello React</div>

export default App

Nothing complex here; we import React, create an App component, which renders the Hello React message, and export it.

Let's now create client.tsx, which is responsible for rendering the App component inside the DOM:

import { render } from 'react-dom'
import App from './app'

render(<App />, document.getElementById('root'))

Again, this should sound familiar, since we import React, ReactDOM, and the App component we created earlier, and we use ReactDOM to render it in a DOM element with the app ID.

Let's now move to the server.

The first thing to do is to create a template.ts file, which exports a function that we will use to return the markup of the page that our server will give back to the browser:

export default body => `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<div id="root">${body}</div>
<script src="/bundle.js"></script>
</body>
</html>`

It should be pretty straightforward. The function accepts body, which we will later see contains the React app, and it returns the skeleton of the page.

It is worth noting that we load the bundle on the client side even if the app is rendered on the server side. SSR is only half of the job that React does to render our application. We still want our application to be a client-side application, with all the features we can use in the browser, such as event handlers, for example.

After this, you need to install express, react, and react-dom:

npm install express react react-dom @types/express @types/react @types/react-dom

Now it is time to create server.tsx, which has more dependencies and is worth exploring in detail:

import React from 'react'
import
express, { Request, Response } from 'express'
import { renderToString } from 'react-dom/server'
import path from 'path'
import App from './App'
import template from './template'

The first thing that we import is express, the library that allows us to create a web server with some routes easily, and which is also able to serve static files.

Secondly, we import React and ReactDOM to render App, which we import as well. Notice the /server path in the import statement of ReactDOM. The last thing we import is the template we defined earlier.

Now we create an Express application:

const app = express()

We tell the application where our static assets are stored:

app.use(express.static(path.resolve(__dirname, './dist/public')))

As you may have noticed, the path is the same that we used in the client configuration of webpack as the output destination of the client bundle.

Then, here comes the logic of SSR with React:

app.get('/', (req: Request, res: Response) => {
const body = renderToString(<App />)
const html = template(body)
res.send(html)
})

We are telling Express that we want to listen to the / route, and when it gets hit by a client, we render App to a string using the ReactDOM library. Here comes the magic and simplicity of the SSR of React.

What renderToString does is return a string representation of the DOM elements generated by our App component; the same tree that it would render in the DOM if we were using the ReactDOM render method.

The value of the body variable is something like the following:

<div data-reactroot="" data-reactid="1" data-react-checksum="982061917">Hello React</div>

As you can see, it represents what we defined in the render method of App, except for a couple of data attributes that React uses on the client to attach the client-side application to the server-side rendered string.

Now that we have the SSR representation of our app, we can use the template function to apply it to the HTML template and send it back to the browser within the Express response.

Last, but not least, we have to start the Express application:

app.listen(3000, () => {
console.log('Listening on port 3000')
})

We are now ready to go; there are only a few operations left. The first one is to define the start script of npm and set it to run the node server:

"scripts": {
"build": "webpack",
"start": "node ./dist/server"
}

The scripts are ready, so we can first build the application with the following command:

npm run build 

When the bundles are created, we can run the following command:

npm start

Point the browser to http://localhost:3000 and see the result.

There are two important things to note here. First, when we use the View Page Source feature of the browser, we can see the source code of the application being rendered and returned from the server, which we would not see if SSR was not enabled.

Second, if we open DevTools and we have the React extension installed, we can see that the App component has been booted on the client as well.

The following screenshot shows the source of the page:

Great! Now that you have created your first React application using SSR, let's learn how to fetch data in the next section.

Implementing data fetching

The example in the previous section should explain clearly how to set up a universal application in React. It is pretty straightforward, and the main focus is on getting things done.

However, in a real-world application, we will likely want to load some data instead of a static React component, such as App in the example. Suppose we want to load Dan Abramov's gists on the server and return the list of items from the Express app we just created.

In the data fetching examples in Chapter 6, Managing Data, we looked at how we can use useEffect to fire the data loading. That wouldn't work on the server because components do not get mounted on the DOM and the life cycle Hook never gets fired.

Using Hooks that were executed earlier will not work either because the data fetching operation is async, while renderToString is not. For that reason, we have to find a way to load the data beforehand and pass it to the component as props.

Let's look at how we can take the application from the previous section and change it a bit to make it load gists during the SSR phase.

The first thing to do is to change App.tsx to accept a list of gists as prop, and loop through it in the render method to display their descriptions:

import { FC } from 'react'

type Gist = {
id: string
description: string
}

type Props = {
gists: Gist[]
}

const App: FC<Props> = ({ gists }) => (
<ul>
{gists.map(gist => (
<li key={gist.id}>{gist.description}</li>
))}
</ul>
)

export default App

Applying the concept that we learned in the previous chapter, we define a stateless functional component, which receives gists as a prop and loops through the elements to render a list of items. Now, we have to change the server to retrieve gists and pass them to the component.

To use the fetch API on the server side, we have to install a library called isomorphic-fetch, which implements the fetch standards. It can be used in Node.js and the browser:

npm install isomorphic-fetch @types/isomorphic-fetch

We first import the library into server.tsx:

import fetch from 'isomorphic-fetch'

The API call that we want to make looks as follows:

fetch('https://api.github.com/users/gaearon/gists') 
.then(response => response.json())
.then(gists => {})

Here, gists are available to be used inside the last then function. In our case, we want to pass them down to App.

Therefore, we can change the / route as follows:

app.get('/', (req, res) => { 
fetch('https://api.github.com/users/gaearon/gists')
.then(response => response.json())
.then(gists => {
const body = renderToString(<App gists={gists} />)
const html = template(body)

res.send(html)
})
})

Here, we first fetch gists, and then we render App to a string, passing the property.

Once App is rendered, and we have its markup, we use the template we used in the previous section and return it to the browser.

Run the following command in the console and point the browser to http://localhost:3000. You should be able to see a server-side render list of gists:

npm run build && npm start

To make sure that the list is rendered from the Express app, you can navigate to view-source:http://localhost:3000 and you will see the markup and the descriptions of gists.

That is great, and it looks easy, but if we check the DevTools console, we can see the Cannot read property 'map' of undefined error. The reason we see the error is that, on the client, we are rendering App again, but without passing gists to it.

This could sound counter-intuitive in the beginning because we might think that React is smart enough to use gists rendered within the server-side string on the client. But that is not what happens, so we have to find a way to make gists available on the client side as well.

You may consider that you can execute the fetch again on the client. That would work, but it is not optimal because you would end up firing two HTTP calls, one on the Express server and one in the browser. If we think about it, we already made the call on the server, and we have all the data we need. A typical solution to sharing data between the server and the client is dehydrating the data in the HTML markup and hydrating it back in the browser.

This seems like a complex concept, but it is not. We will now look at how easy it is to implement. The first thing we must do is to inject gists in the template after we have fetched them on the client.

To do this, we have to change the template slightly as follows:

export default (body, gists) => ` 
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<div id="root">${body}</div>
<script>window.gists = ${JSON.stringify(gists)}</script>
<script src="/bundle.js"></script>
</body>
</html>
`

The template function now accepts two parameters—body of the app and the collection of gists. The first one is inserted inside the app element, while the second is used to define a global gists variable attached to the window object so that we can use it in the client.

Inside the Express route (server.js), we just have to change the line where we generate the template passing the body, as follows:

const html = template(body, gists)

Last, but not least, we have to use gists attached to a window inside client.tsx, which is pretty easy:

ReactDOM.hydrate( 
<App gists={window.gists} />,
document.getElementById('app')
)

Hydrate was introduced in React 16 and works similar to render on the client side, irrespective of whether the HTML has server-rendered markup or not. If there is no markup previously using SSR, then the hydrate method will fire a warning that you can silence it by using the new suppressHydrationWarning attribute.

We read gists directly, and we pass them to the App component that gets rendered on the client.

Now, run the following command again:

npm run build && npm start

If we point the browser window to http://localhost:3000, the error is gone, and if we inspect the App component using React DevTools, we can see how the client-side App component receives the collection of gists.

As we have created our first SSR application, let's now see how we can do this more easily by using an SSR framework called Next.js in the next section.

Using Next.js to create a React application

You have looked at the basics of SSR with React, and you can use the project we created as a starting point for a real app. However, you may think that there is too much boilerplate and that you are required to know too many different tools to run a simple universal application with React. This is a common feeling called JavaScript fatigue, as described in the introduction to this book.

Luckily, Facebook developers and other companies in the React community are working very hard to improve the DX and make the life of developers easier. You might have used create-react-app at this point to try out the examples in the previous chapters, and you should understand how it makes it very simple to create React applications without requiring developers to learn many technologies and tools.

Now, create-react-app does not support SSR yet, but there's a company called Vercel that has created a tool called Next.js, which makes it incredibly easy to generate universal applications without worrying about configuration files. It also reduces the boilerplate a lot.

It is important to say that using abstractions is always very good for building applications quickly. However, it is crucial to know how the internals work before adding too many layers, and that is why we started with the manual process before learning Next.js. We have looked at how SSR works and how we can pass the state from the server to the client. Now that the base concepts are clear, we can move to a tool that hides a little bit of complexity and makes us write less code to achieve the same results.

We will create the same app where all gists from Dan Abramov are loaded, and you will see how clean and simple the code is, thanks to Next.js.

First of all, create a new project folder (you can call it next-project) and run the following command:

npm init

When this is done, we can install the Next.js library and React:

npm install next react react-dom typescript @types/react @types/node

Now that the project is created, we have to add an npm script to run the binary:

"scripts": { 
"dev": "next"
}

Perfect! It is now time to generate our App component.

Next.js is based on conventions, with the most important one being that you can create pages to match the browser URLs. The default page is index, so we can create a folder called pages and put an index.js file inside it.

We start importing the dependencies:

import fetch from 'isomorphic-fetch'

Again, we import isomorphic-fetch because we want to be able to use the fetch function on the server side.

We then define a component called App:

const App = () => {

}

export default App

Then we define a static async function, called getInitialProps, which is where we tell Next.js which data we want to load, both on the server side and on the client side. The library will make the object returned from the function available as props inside the component.

The static and async keywords applied to a class method mean that the function can be accessed outside the instance of the class and that the function yields the execution of the wait instructions inside its body.

These concepts are pretty advanced, and they are not part of the scope of this chapter, but if you are interested in them, you should check out the ECMAScript proposals (https://github.com/tc39/proposals).

The implementation of the method we just described is as follows:

App.getInitialProps = async () => { 
const url = 'https://api.github.com/users/gaearon/gists'
const response = await fetch(url)
const gists = await response.json()

return {
gists
}
}

We are telling the function to fire the fetch and wait for the response; then we are transforming the response into JSON, which returns a promise. When the promise is resolved, we can return the props object with gists.

render of the component looks pretty similar to the preceding one:

return ( 
<ul>
{props.gists.map(gist => (
<li key={gist.id}>{gist.description}</li>
))}
</ul>
)

Before you run the project, you need to configure tsconfig.json:

{
"compilerOptions": {
"baseUrl": "src",
"esModuleInterop": true,
"module": "esnext",
"noImplicitAny": true,
"outDir": "dist",
"resolveJsonModule": true,
"sourceMap": false,
"target": "es6",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"moduleResolution": "node",
"isolatedModules": true,
"jsx": "preserve"
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
}

Now, open the console and run the following command:

npm run dev

We will see the following output:

> Ready on http://localhost:3000

If we point the browser to that URL, we can see the universal application in action. It is really impressive how easy it is to set up a universal application with a few lines of code and zero-configuration, thanks to Next.js.

You may also notice that if you edit the application inside your editor, you will be able to see the results within the browser instantly without needing to refresh the page. That is another feature of Next.js, which enables hot module replacement. It is incredibly useful in development mode.

If you liked this chapter, go and give a star on GitHub: https://github.com/zeit/next.js.

Summary

The journey through SSR has come to an end. You are now able to create a server-side rendered application with React, and it should be clear why it can be useful for you. SEO is certainly one of the main reasons, but social sharing and performance are important factors as well. You learned how it is possible to load the data on the server and dehydrate it in the HTML template to make it available for the client-side application when it boots on the browser.

Finally, you have looked at how tools such as Next.js can help you reduce the boilerplate and hide some of the complexity that setting up a server-side render React application usually brings to the code base.

In the next chapter, we will talk about how to improve the performance of our React applications.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
35.173.178.60