DEV Community

Markus Claus
Markus Claus

Posted on

Server-side rendering with React (and Redux)

When I first started react, I was amazed by how powerful the library was. I was building component after component and when I came upon a share component for Facebook, I noticed "Oh, there is no content on the page at all!"

That's not good. Is it? So what's up with that?

Most automated site crawlers don't execute JavaScript before they read the website.
(Google might, but I couldn't confirm it). And with React there is no content on the page before your JavaScript is executed. That means that crawlers can't see anything on your website.

Also, React isn't the fastest way to get content to your visitors. If you look into the network tab in your browsers developer tools, you will notice that it requests the page, then the JavaScript bundle - it is executed, and in most cases after that, the real content is requested by your React app.

These are a lot of calls happening before your visitor can see any content.

So, with these two problems in mind, what can we do?

Server-side rendering in a nutshell

There is a super cool technique called server-side rendering. What it does is pretty simple: It executes your React app on the server and sends the HTML result to your visitor's client. The client renders the HTML and then requests your JavaScript. Rather than rendering the app again, it hydrates the HTML, already sent, with all the functions you got in your React app.

With this approach the content does not need any JavaScript execution by the client to show. Also, you dont need three requests for the content to arrive. Everything the visitor should see is sent with the initial call.

Getting ready

The above explanation sounds pretty simple. Unfortunately, it isn't. The implementation has some complex parts we need to understand, but I try to keep them as simple and well explained as I can.

Before we start, you need to install webpack, webpack-cli, webpack-merge, webpack-node-external, express, babel, babel-preset-env, babel-preset-react, babel-loader and of course react and react-dom. That should do it, for now.

Now we need to set up Webpack. Webpack is a module bundler. Most of you people use it, I suppose, or at least have heard of it. I don't want to go to deep into webpack, it's large enough for its own article (...or five).

For the purpose of server-side rendering, we will use a more complex setup for our webpack. We are going to split the webpack configuration into multiple files, so we do not need to repeat to much code.

In the end we'll need two webpack setups. One for our server-side code and one for the client. These two files have much in common, so we create one file called webpack.base.js which contains most of the configuration both setups share.



// webpack.base.js

module.exports = {
  stats: {
    colors: true,
    reasons: true,
    chunks: false
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        loader: 'babel-loader',
        exclude: /node_modules/
      },
    ]
  }
}


Enter fullscreen mode Exit fullscreen mode

What you see here is a standard configuration for webpack. You export a JavaScript object which contains several configuration information for webpack. The stats part is for some fixes in the output of webpack, the module part tells webpack what type of loaders it should use when encountering a file which fulfills the "test" regex. A loader is a function which transforms the content of the file in some ways.

Okay, I was talking about more than one setup. Let's do the server-side first, because we start working on the server very soon.



// webpack.server.js

const path = require('path');
const merge = require('webpack-merge');
const baseConfig = require('./webpack.base.js');

const config = {
    target: 'node',
    entry:  ['./index.js'],
    output:  {
        filename: 'server.js',
        path: path.resolve(__dirname, 'build'),
    },
    resolve: {
        extensions: ['.js', '.json']
    },
}

module.exports = merge(baseConfig, config);


Enter fullscreen mode Exit fullscreen mode

This is our server-side configuration, right at the end of the file you can see, that we merge the baseConfig object from our webpack.base.js file with the content in this file to get our final webpack config for the server side.

The entry key tells what the starting point webpack should look for code. And the output key tells it to where the bundled file should be saved to.

Before we are getting into our server app, let's set up Babel real fast:



// .babelrc

{
    "presets": [
        "react",
    ["env", {
        "targets": {
            "browsers": "last 2 versions"
        },
            "useBuiltIns": "usage",
        "loose": true,
        "modules": false
        }]
    ],

}


Enter fullscreen mode Exit fullscreen mode

I can't go into details what this is doing. The article would grow far too big. In few words, it sets up Babel to understand common react stuff.

And let's tell npm about webpack and the setup we just created. In the script section of your package.json add:



// package.json add to scripts

{
    scripts: {
        ...
        "server": "webpack --watch --config webpack.server.js --mode development"
    }
}



Enter fullscreen mode Exit fullscreen mode

Let's get coding

The next step is to set up some kind of server to respond to our visitor's requests. I will use Express for this article but you can use any server you want.

Let's install Express using npm install express.

After that we set up a server:



// index.js

import express from 'express';
const app = express();

app.use(express.static('public'));

app.get('*', (req, res) => {
    // Here we are going to handle that react rendering and stuff.
});

app.listen(3000, () => {
    console.log('Server is listenting to port 3000');
});



Enter fullscreen mode Exit fullscreen mode

What we did - we set up an express server, which is listening on port 3000. We added public as a folder for static resources. We also created a GET route with a wildcard. In this case it means that every call made is matched with this route.

You may have noticed that I bring in Express via import rather than require. That's because we want to write Isomorphic JavaScript. That's JavaScript that can be run on the server and the client without changes.

Now we want to create something we can actually render on the server. Let's do some kind of Homepage view component the user should see when he visits our app.



// client/Homepage.js

import React from 'react';

class Homepage extends React.Component {

    render() {
        return (
            <div class="wrapper">
                <h1>Welcome, this is the homepage</h1>
                <button onClick={() => console.log("I', clicked!"}>
                    Click me!
                </button>
            </div>
        )
    }

}

export default Homepage;



Enter fullscreen mode Exit fullscreen mode

Pretty simple, isn't it? The only thing I want to note here is that I put all the client code into a folder client. Always keep your code well sorted, people!

We got our server, we got our React component, let's bring those together.



// renderer.js

import { renderToString } from 'react-dom/server';
import Homepage from './client/Homepage';

function renderer() {

    const content = renderToString(
        <Homepage />
      );

    return content;
}

export default renderer;



Enter fullscreen mode Exit fullscreen mode

To keep everything nice and clean, we put our rendering code into a function called renderer. As you can see, it imports a special function from react-dom/server called renderToString(). Render to string is similar to render() you propably know about. It renders the component a single time and converts it into a HTML string.

After that we call renderer() in our Express server and send the result back to the visitor.



// index.js

import express from 'express';
import renderer from './renderer';

const app = express();

app.get('*', (req, res) => {
    const result = renderer();
    res.send(result);
});

app.listen(3000, () => {
    console.log('Server is listenting to port 3000');
});



Enter fullscreen mode Exit fullscreen mode

Voilá, the first rendered React component on the server. You will notice that no matter which route you call, the Homepage component is rendered every time. That's okay for now, we tackle this one in a minute. For now be happy, we got server-side rendering up and running.

Where has my JavaScript gone?!?

You probably tried to use the button on the homepage and you might have noticed "Oh, there is no console log... Where has my JavaScript gone!?!".

It's gone because of renderToString(). It renders a HTML string, but it removes every bit of JavaScript. To fix this, we need to create a client bundle for our app and request it from the visitors client as soon as it receives the HTML code.

To do this, let's go back to Webpack. We need the earlier mentioned client-side setup. We are doing this because some of the server code is not necessary on the client or maybe we have some super secret business logic in there we don't want to share or maybe API secrets... You get the point.



// webpack.client.js

const webpack = require('webpack');
const path = require('path');
const merge = require('webpack-merge');
const baseConfig = require('./webpack.base.js');

const config = {
    context: __dirname,
    entry: ['./client/index.js'],
    output: {
        path: path.join(__dirname, 'public'),
        filename: 'bundle.js',
    publicPath: '/',
    pathinfo: false,
    },
    resolve: {
        extensions: ['.js', '.json', '.scss']
    },
    optimization: {
        minimize: true
    },
};

module.exports = merge(baseConfig, config);


Enter fullscreen mode Exit fullscreen mode

As with the server-side before, we build a client configuration and then merge it with our base configuration. Important to note is, that we use another entry point and we create another output file. So, after running webpack in both configurations we end up with two files: server.js and bundle.js

To start webpack we need to add a new script to npm:



// package.json add to scripts

{
    scripts: {
        ...
        "client": "webpack --watch --config webpack.client.js --mode development"
    }
}


Enter fullscreen mode Exit fullscreen mode

For webpack to actually build our bundle.js we need to write it first. So here we go:



// client/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import Homepage from './homepage';

const jsx = <Homepage />

ReactDOM.hydrate(jsx, document.getElementById('app'));



Enter fullscreen mode Exit fullscreen mode

This is a pretty standard setup for a React app. We use our Homepage component and we are rendering it into a div with the ID "app". Because we are talking about the client-side here, when this code arrives, the page is already rendered. To make React understand, we don't want it to rerender the whole thing, we need to use hydrate instead of render to tell React to just put some life in our HTML code.

Now we have our JavaScript file to serve to the client, but the file is still not showing up when we refresh our page. That's because we need to tell the client to actually request the client.js file. To do this, we add a little bit of code to our renderer() function:



// renderer.js

import { renderToString } from 'react-dom/server';
import Homepage from './client/Homepage';

function renderer() {

    const content = renderToString(
        <Homepage />
      );

    const jsx = `
        <!DOCTYPE html>
        <html>
        <head>
            <meta charset="utf-8">
            <meta name="viewport" content="width=device-width, initial-scale=1">
        </head>

        <body>
            <div id="app">${content}</div>
            <script src="/bundle.js"></script>
        </body>
        </html>
    `;

    return jsx;
}

export default renderer;


Enter fullscreen mode Exit fullscreen mode

Our renderer returns no simple React component anymore, but a complete website with the bundle.js included. As soon as the client renders the HTML code, it will request our JavaScript with the whole code and the button you wanted to create will start working. Or at least it should ;)

Routers, routers everywhere!

Now that we got a real React app in the frontend rendered by our server, we can talk about the routing problem. Do you remember? No matter what route we use, we always end up on our Homepage. To solve this we install a tool called react-router. I assume you people know how a BrowserRouter works. The thing is... We can't use a BrowserRouter because it's specifically used to render routes depending on the browser url. But we got no browser on the server. So we are in a bit of a pickle here.

What we are going to do is, we use a StaticRouter for the server part instead and good ol' browser router for the frontend.

But we don't want to write two seperate route declarations whenever we add a new route to our software, do we?

So let's put all our routes in one file and use this to configure both routers. To do this there is a little tool called react-router-config. It allows us to write an configuration array and provides a function to render the routes.



// routes.js

import Homepage from './client/Homepage';
import AboutUs from './client/AboutUs';

const routes = [
    {
        component: Homepage,
        path: "/",
        exact: true
    },
    {
        component: AboutUs,
        path: "/",
        exact: true
    }    
]

export default routes;



Enter fullscreen mode Exit fullscreen mode

Each object in the array takes the same parameters as a Route component from react-router would do.

In our client code we now can do the following to get our BrowserRouter:



// client/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import { renderRoutes } from 'react-router-config';

import routes from '../routes.js';

const jsx = (
    <Router>
      <div class="wrapper">{renderRoutes(routes)}</div>
    </Router>
);


ReactDOM.hydrate(jsx, document.getElementById('app'));



Enter fullscreen mode Exit fullscreen mode

We use
renderRoutes from react-router-config and pass it our routes array. The result will be a completly rendered pack of Route components.

The server-side is a little bit more complex. Here we use the StaticRouter. The static router has no browser url, so we need to pass it a request, where it can get the neccessary information which route we want to render.

The request is passed from our express app into our renderer function we wrote earlier to render the website.



// renderer.js

import { renderToString } from 'react-dom/server';
import { StaticRouter as Router } from 'react-router-dom';
import { renderRoutes } from 'react-router-config';

import routes from './routes.js';

function renderer(req) {

    const context = {};

    const content = renderToString(
        <Router context={context} location={req.path} query={req.query}>
            <div>{renderRoutes(routes)}</div>
        </Router>
    );

    const jsx = `
        <!DOCTYPE html>
        <html>
        <head>
            <meta charset="utf-8">
            <meta name="viewport" content="width=device-width, initial-scale=1">
        </head>

        <body>
            <div id="app">${content}</div>
            <script src="/bundle.js"></script>
        </body>
        </html>
    `;

    return {jsx, context};
}

export default renderer;


Enter fullscreen mode Exit fullscreen mode

As you can see, we do pretty much the same as on the client side but we use the StaticRouter instead. We pass it the location from the request so it can determine what to do. Also we give it a query so we can react on paramters in the request. The last thing is the context. The context is accessible in the component the router renders. You can use it for error handling or to set a status for the page rendered.

For example, let's say you want to load a product from an API and the requested product is not there. You want the initial call from the server answer with a 404 status code instead of a 200. That's where you can use the context. In the component just set the context.status = 404 and after the rendering set res.status(context.status) like this:



// index.js

import express from 'express';
import renderer from './renderer';

const app = express();

app.get('*', (req, res) => {
    const result = renderer(req);
    const context = result.context;
    if(context && context.status !== undefined) {
        res.status(context.status);
    }
    res.send(result.jsx);
});

app.listen(3000, () => {
    console.log('Server is listenting to port 3000');
});



Enter fullscreen mode Exit fullscreen mode

And here we go. The routing in your app works. Client-side as well as server-side. When new routes are created, all you need to do is to add them into the routes array and both routers know what to do.

Getting the Redux on

Do your brains hurt right now? No? Good. Because I saved the most annoying (yes, annoying!) part for last: Redux.

In fact, it's not just Redux I want to talk about last, I want to talk about all sorts of async data fetching when rendering a React app server-side. The problem is, as you remember, we use renderToString to prerender our page and then send it to the client. But renderToString does render everything just once and therefore does not care about data we might need to fetch from an API before it renders. But we want search engines and possibly other tools to have those juicy data parts we are missing.

So what we need to do is in fact get all the data we need to render a complete view before we actually call renderToString. So, for every view we need to put together, some kind of "content fetch" function which loads every single piece of content the requested view needs.

Then, after the content is available, we need to pass the data to our React app and then, with everything available, call renderToString.

First we put Redux in the mix: npm install redux react-redux redux-thunk

Now that we have installed Redux, let's first do the easy part, the frontend.

We are going to use a somehow non-standard approach here, but you will understand why we do it like this in the end.




// createStore.js

import { applyMiddleware, createStore } from 'redux';
import thunk from 'redux-thunk';

import rootReducer from './rootReducer';

const middleware = applyMiddleware(thunk);

export default initialState => createStore(rootReducer, initialState, middleware);



Enter fullscreen mode Exit fullscreen mode

We wrote a function that takes an initialState and returns the created store. We will use this on the client- and server-side as well.



// client/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import { renderRoutes } from 'react-router-config';

import {Provider} from 'react-redux';

import createStore from '../createStore';
import routes from '../routes';
import rootReducer from '../rootReducer';

const store = createStore({});

const jsx = (
    <Provider store={store}>
        <Router>
          <div class="wrapper">{renderRoutes(routes)}</div>
        </Router>
    </Provider>
);


ReactDOM.hydrate(jsx, document.getElementById('app'));



Enter fullscreen mode Exit fullscreen mode

We created a store with our new createStore() function and wrapped the app in a Provider.

To have everything go as we plan, we need to have all the data we need for the view to render after we are finished with our data loading process. As long as you have a view component, where all the data is loaded and then passed down to child components, it's pretty easy to spot what we need to load. If you have many components in your view that load data by themselves, it can get hard not to forget some fetches here and there.

To give us an opportunity to load data, we need a function that is loading the data so we can call that before we render. To achieve this, we need to rewrite our view component a little bit.

I added a connection to the Redux store. I don't want to talk about how to do this and what this all means, I wrote an article about it a week ago.



Feel free to read it!



// client/Homepage.js

import React from 'react';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';

import fetchSomeData from './somewhere';

class Homepage extends React.Component {
    componentDidMount() {
        const {fetchSomeData: doFetch} = this.props;
        doFetch();
    }

    render() {
        return (
            <div class="wrapper">
                <h1>Welcome, this is the homepage</h1>
                <button onClick={() => console.log("I', clicked!"}>
                    Click me!
                </button>
            </div>
        )
    }

}

const mapStateToProps = state => ({
    someData: getSomeData(state);
})

const mapDispatchToProps = dispatch => bindActionCreators({
    fetchSomeData,
}, dispatch);


function loadData(store, match, cookie) {
    const actionToBeDispatched = [];
    actionToBeDispatched.push(store.dispatch(fetchSomeData()));

    return Promise.all(actionsToBeDispatched);
}

export default {
  loadData,
  component: connect(mapStateToProps, mapDispatchToProps)(Homepage),
};



Enter fullscreen mode Exit fullscreen mode

In this code example there are two things that are really important. First we got the loadData() function. Its purpose is to load every single bit of data we need. loadData() returns a Promise which lets us know when everything is done.

Also, we don't export our component like we did before, but we export an object with the loadData() and the component itself. This object will be split in our routes so every route still has a component but also got a loadData key.



// routes.js

import Homepage from './client/Homepage';
import AboutUs from './client/AboutUs';

const routes = [
    {
        ...Homepage,
        path: "/",
        exact: true
    },
    {
        ...AboutUs,
        path: "/",
        exact: true
    }    
]

export default routes;



Enter fullscreen mode Exit fullscreen mode

Okay, let's bring everything together!



// index.js

import express from 'express';
import { matchRoutes } from 'react-router-config';
import renderer from './renderer';
import createStore from './createStore';

const app = express();

app.get('*', (req, res) => {
    const store = createStore({});
    const promises = matchRoutes(routes, req.path).map(
        ({ route, match }) => (route.loadData ? route.loadData(store, match,             req.get('cookie') || {}, req.query) : null)
    );

    Promise.all(promises).then(() => {
        const result = renderer(req, store);
        const context = result.context;
        if(context && context.status !== undefined) {
            res.status(context.status);
        }
        res.send(result.jsx);
    });
});

app.listen(3000, () => {
    console.log('Server is listenting to port 3000');
});



Enter fullscreen mode Exit fullscreen mode

In our route we create a store using createStore we wrote earlier. Then we use a function called matchRoutes() which takes our routes array and the requests path. It returns an array of matched routes. Every item contains the route and the match data from react-router.

With this we can call our loadData() function which is part of the routes object and wait for it to finish loading data. After that we pass the store to our renderer() function to redner everything properly.

loadData() in this example takes a cookie and a query parameter. They are not neccessary, but I decided to leave them in there so you poeple can see how you would add a cookie or query parameters to your loadData() function.

We are nearly done. Last thing to do is to make sure that the client is in the same state as the server when it loads up. Right now the server would set up everything correctly, then send it to the client. But the client would mess things up, because it has no idea what the server-side store contains. So we need to send the store to the client.

Let's update our renderer()



// renderer.js

import { renderToString } from 'react-dom/server';
import { StaticRouter as Router } from 'react-router-dom';
import { renderRoutes } from 'react-router-config';

import routes from './routes.js';

function renderer(req, store) {

    const context = {};
    const state = store.getState();

    const content = renderToString(
        <Router context={context} location={req.path} query={req.query}>
            <div>{renderRoutes(routes)}</div>
        </Router>
    );

    const jsx = `
        <!DOCTYPE html>
        <html>
        <head>
            <meta charset="utf-8">
            <meta name="viewport" content="width=device-width, initial-scale=1">
        </head>

        <body>
            <div id="app">${content}</div>
            <script>
                window.STORE_DATA = ${JSON.stringify(state).replace('<script>', '')}
            </script>
            <script src="/bundle.js"></script>
        </body>
        </html>
    `;

    return {jsx, context};
}

export default renderer;


Enter fullscreen mode Exit fullscreen mode

We use JSON.stringify() to put the whole store into one variable.
We remove any script tags, because they would mess up things and are potential security issues.

Do you remember that we wrote a function called createStore() which didn't make that much sense back there because...duhh... redundant? Now that our state is in a variable, we can take that state and put it into our createStore() function to create a store from this state. And the penny drops ;)



// client/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import { renderRoutes } from 'react-router-config';

import {Provider} from 'react-redux';

import createStore from '../createStore';
import routes from '../routes';
import rootReducer from '../rootReducer';

const store = createStore(window.STORE_DATA);

const jsx = (
    <Provider store={store}>
        <Router>
          <div class="wrapper">{renderRoutes(routes)}</div>
        </Router>
    </Provider>
);


ReactDOM.hydrate(jsx, document.getElementById('app'));



Enter fullscreen mode Exit fullscreen mode

Okay. That's it. It was a long road, but we are finally at the end. Our React app is working, the routing is set up, the Redux store is there and we have a server-side rendered app that is hydrated on the client. You can test this by deactivating JavaScript in your browser. The page should show just as you want it to be, but without any JavaScript functions.

There are a few things I didn't tackle here. What about authentication at the frontend. You can make this work using cookies, but it's still a little bit tricky. The article is long enough, maybe I will talk about this another time.

As always, I hope you learned something, I hope you enjoyed reading the article and if you did, feel free to like it or leave a comment. I always appreciate hints, tips or just a friendly "hello".

Thanks for reading!

Top comments (2)

Collapse
 
githubber profile image
git-hubber

Thanks Markus. Is this code available on github?

I'm having troubles getting this to work.

Collapse
 
mohannadelqadi profile image
mohannadelqadi

Your code is not working my friend