Why server Side Rendering (SSR)
The following points are summarized:
- SEO makes it easier for search engines to read page content
- The first screen rendering is faster (important) and there is no need to wait for the JS file to download and execute
- The code is homogeneous, and the server and client can share some code
Today we will build a simple React application using Redux to implement server-side rendering (SSR). The example includes asynchronous data fetching, which makes the task more interesting.
If you want to use the code discussed in this article, check out GitHub: answer518/ React-redux-SSR
Installation environment
Before we start writing the application, we need to configure the environment compile/package environment, since we are writing the code using ES6 syntax. We need to compile the code into ES5 code for execution in a browser or node environment.
We’ll use the Babelify transform to package our client code using Browserify and Watchify. For our server-side code, we will use babel-CLI directly.
The code structure is as follows:
Build SRC ├─ ├─ custom.txt ├─ custom.txtCopy the code
We added the following two command scripts to package.json:
"scripts": { "build": " browserify ./src/client/client.js -o ./build/bundle.js -t babelify && babel ./src/ --out-dir ./build/", "watch": " concurrently \"watchify ./src/client/client.js -o ./build/bundle.js -t babelify -v\" \"babel ./src/ --out-dir ./build/ --watch\" " }Copy the code
The Concurrently library helps run multiple processes in parallel, which is what we need when monitoring changes.
One last useful command to run our HTTP server:
"scripts": { "build": "..." , "watch": "..." , "start": "nodemon ./build/server/server.js" }Copy the code
The reason for using Nodemon instead of Node./build/server/server.js is that it can monitor any changes in our code and automatically restart the server. This can be very useful during development.
Develop the React+Redux application
Suppose the server returns the following data format:
[
{
"id": 4,
"first_name": "Gates",
"last_name": "Bill",
"avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/marcoramires/128.jpg"
},
{
...
}
]
Copy the code
We render the data from a component. In the componentWillMount lifecycle method for this component, we will trigger the data fetch, and once the request is successful, we will send an operation of type user_Fetch. This operation will be handled by a Reducer and we will get updates in the Redux store. A change in state triggers our component to rerender the specified data.
Redux concrete implementation
Reducer processing process is as follows:
// reducer.js
import { USERS_FETCHED } from './constants';
function getInitialState() {
return { users: null };
}
const reducer = function (oldState = getInitialState(), action) {
if (action.type === USERS_FETCHED) {
return { users: action.response.data };
}
return oldState;
};
Copy the code
To send out action requests to change the state of the application, we need to write action Creator:
// actions.js
import { USERS_FETCHED } from './constants';
export const usersFetched = response => ({ type: USERS_FETCHED, response });
// selectors.js
export const getUsers = ({ users }) => users;
Copy the code
The most critical step in Redux implementation is to create a Store:
// store.js
import { USERS_FETCHED } from './constants';
import { createStore } from 'redux';
import reducer from './reducer';
export default () => createStore(reducer);
Copy the code
Why is the factory function returned directly instead of createStore(Reducer)? This is because when we render on the server side, we need a brand new Store instance to handle each request.
Implementing the React component
One important point to make here is that once we want to implement server-side rendering, we need to change our previous pure client-side programming model.
Server-side rendering, also known as code isomorphism, means that the same code can be rendered on both the client and the server.
We must ensure that the code runs properly on the server. For example, Node does not provide access to the Window object.
// App.jsx
import React from 'react';
import { connect } from 'react-redux';
import { getUsers } from './redux/selectors';
import { usersFetched } from './redux/actions';
const ENDPOINT = 'http://localhost:3000/users_fake_data.json';
class App extends React.Component {
componentWillMount() {
fetchUsers();
}
render() {
const { users } = this.props;
return (
<div>
{
users && users.length > 0 && users.map(
// ... render the user here
)
}
</div>
);
}
}
const ConnectedApp = connect(
state => ({
users: getUsers(state)
}),
dispatch => ({
fetchUsers: async () => dispatch(
usersFetched(await (await fetch(ENDPOINT)).json())
)
})
)(App);
export default ConnectedApp;
Copy the code
As you can see, we’re using componentWillMount to send fetchUsers requests, why doesn’t componentDidMount work? The main reason is that componentDidMount does not execute during server-side rendering.
FetchUsers is an asynchronous function that requests data through the Fetch API. When the data returned, the users_fetch action was sent to recalculate the state through the Reducer, and our
// client.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App.jsx';
import createStore from './redux/store';
ReactDOM.render(
<Provider store={ createStore() }><App /></Provider>,
document.querySelector('#content')
);
Copy the code
Run the Node Server
For demonstration purposes, we prefer Express as the HTTP server.
// server.js import express from 'express'; const app = express(); // Serving the content of the "build" folder. Remember that // after the transpiling and bundling we have: / / / / build / / ├ ─ ─ client / / ├ ─ ─ server / / │ └ ─ ─ for server js / / └ ─ ─ bundle. Js app. Use (express. Static (__dirname + '/.. / ')); app.get('*', (req, res) => { res.set('Content-Type', 'text/html'); res.send(` <html> <head> <title>App</title> </head> <body> <div id="content"></div> <script src="/bundle.js"></script> </body> </html> `); }); app.listen( 3000, () => console.log('Example app listening on port 3000! '));Copy the code
With this file, we can run NPM run start and visit http://localhost:3000. We see the data get successful and display successfully.
Server side rendering
So far, our server has just returned an HTML skeleton, and all the interaction is done on the client side. The browser needs to download bundle.js and execute it. The purpose of server-side rendering is to do all the work on the server and send the final tag, rather than handing everything over to the browser. React is smart enough to recognize these markers.
Remember what we did on the client side?
import ReactDOM from 'react-dom';
ReactDOM.render(
<Provider store={ createStore() }><App /></Provider>,
document.querySelector('#content')
);
Copy the code
The server is almost the same:
import ReactDOMServer from 'react-dom/server';
const markupAsString = ReactDOMServer.renderToString(
<Provider store={ store }><App /></Provider>
);
Copy the code
We use the same component
Then add the string to the Express response, so the server code reads:
const store = createStore();
const content = ReactDOMServer.renderToString(
<Provider store={ store }><App /></Provider>
);
app.get('*', (req, res) => {
res.set('Content-Type', 'text/html');
res.send(`
<html>
<head>
<title>App</title>
</head>
<body>
<div id="content">${ content }</div>
<script src="/bundle.js"></script>
</body>
</html>
`);
});
Copy the code
If you restart the server and open the same http://localhost:3000, we should see the following response:
<html>
<head>
<title>App</title>
</head>
<body>
<div id="content"><div data-reactroot=""></div></div>
<script src="/bundle.js"></script>
</body>
</html>
Copy the code
We do have some content in our page, but it’s just
. That doesn’t mean the program is wrong. This is absolutely true. React does render our page, but it only renders static content. In our component, we have nothing until we fetch the data, which is an asynchronous process, and we have to take this into account when rendering on the server. This is where our task gets tricky. It comes down to what our application is doing. In this case, the client code depends on a specific request, but if you use the Redux-Saga library, it could be multiple requests, or it could be a full root saga. I realized that there are two ways to approach this problem:
1. We know exactly what data is required for the requested page. We take the data and use it to create a Redux store. We then render the page by providing a completed Store, which we could theoretically do.
2. We rely entirely on the code running on the client side to calculate the final result.
The first approach requires state management at both ends. The second approach, which I personally recommend, requires us to use some additional libraries or tools on the server side to ensure that the same set of code does the same thing on the server and client side.
For example, we use the Fetch API to make asynchronous requests to the backend, which is not supported by the server by default. All we need to do is import Fetch in server.js:
import 'isomorphic-fetch';
Copy the code
Once we receive the asynchronous data using the client API, Store access to asynchronous data, we would trigger ReactDOMServer renderToString. It will give us the tag we want. Our Express processor looks like this:
app.get('*', (req, res) => {
const store = createStore();
const unsubscribe = store.subscribe(() => {
const users = getUsers(store.getState());
if (users !== null && users.length > 0) {
unsubscribe();
const content = ReactDOMServer.renderToString(
<Provider store={ store }><App /></Provider>
);
res.set('Content-Type', 'text/html');
res.send(`
<html>
<head>
<title>App</title>
</head>
<body>
<div id="content">${ content }</div>
<script src="/bundle.js"></script>
</body>
</html>
`);
}
});
ReactDOMServer.renderToString(<Provider store={ store }><App /></Provider>);
});
Copy the code
We use the Store subscribe method to listen for state. When the state changes – whether any user data has been retrieved. If users exists, we will unsubscribe() so that we don’t run the same code twice, and we use the same stored instance to convert to string. Finally, we output the markup to the browser.
The store.subscribe method returns a function that can be called to unsubscribe listening
With the above code, our component can now be successfully rendered on the server side. Using the developer tools, we can see the content sent to the browser:
<html> <head> <title>App</title> <style> body { font-size: 18px; font-family: Verdana; } </style> </head> <body> <div id="content"><div data-reactroot=""><p>Eve Holt</p><p>Charles Morris</p><p>Tracey Ramos</p></div></div> <script> window.__APP_STATE = {"users":[{"id":4,"first_name":"Eve","last_name":"Holt","avatar":"https://s3.amazonaws.com/uifaces/faces/twitter/marcora mires/128.jpg"},{"id":5,"first_name":"Charles","last_name":"Morris","avatar":"https://s3.amazonaws.com/uifaces/faces/twi tter/stephenmoon/128.jpg"},{"id":6,"first_name":"Tracey","last_name":"Ramos","avatar":"https://s3.amazonaws.com/uifaces/ faces/twitter/bigmancho/128.jpg"}]}; </script> <script src="/bundle.js"></script> </body> </html>Copy the code
Of course, this isn’t the end of it; the client JavaScript doesn’t know what’s going on on the server or that we’ve made a request to the API. We must notify the browser by passing the state of the Store so that it can receive it.
const content = ReactDOMServer.renderToString(
<Provider store={ store }><App /></Provider>
);
res.set('Content-Type', 'text/html');
res.send(`
<html>
<head>
<title>App</title>
</head>
<body>
<div id="content">${ content }</div>
<script>
window.__APP_STATE = ${ JSON.stringify(store.getState()) };
</script>
<script src="/bundle.js"></script>
</body>
</html>
`);
Copy the code
We put the Store state into a global variable __APP_STATE, and reducer has changed a little:
function getInitialState() { if (typeof window ! == 'undefined' && window.__APP_STATE) { return window.__APP_STATE; } return { users: null }; }Copy the code
Note the Typeof Window! == ‘undefined’, we have to do this because this code will also be executed on the server side, which is why you have to be very careful when doing server-side rendering, especially with browser apis that are used globally.
The last area that needs to be optimized is that the fetch must be prevented when users is already fetched.
componentWillMount() { const { users, fetchUsers } = this.props; if (users === null) { fetchUsers(); }}Copy the code
conclusion
Server-side rendering is an interesting topic. It has many advantages and improves the overall user experience. It will also improve SEO for your one-page app. But none of this is easy. In most cases, additional tools and carefully chosen apis are required.
This is just a simple case, real development scenarios are often much more complex than this, and there are a lot of things to consider. How do you do server-side rendering?