Skip to main content

Simple Implementation of React Server-side Rendering

· 3 min read
3Alan
🤖WARNING
The English translation was done by AI.

Regarding the concept, I think this article explains it well.

Tech Stack

  • React
  • Express
  • Redux

Source Code

View

Overview

Flowchart

Server-side

The server-side mainly handles component rendering (renderToString), Redux, getServerSideProps, and an API that emulates Next.js.

Finding the Corresponding Component

To facilitate unified management of client-side and server-side routing, we maintain a separate routes list.

import React from 'react';
import Detail from './pages/Detail';
import List from './pages/List';

const routes = [
{
path: '/',
element: <List />
},
{
path: '/detail/:name',
element: <Detail />
}
];

export default routes;

First, we obtain the user's accessed URL and use matchRoutes to find the route that matches the URL.

import { renderToString } from 'react-dom/server';
import { matchRoutes, renderMatches } from 'react-router-dom';

app.get('*', (req, res) => {
const matchedComponent = matchRoutes(routes, req.url);

let reduxState = {};
if (matchedComponent) {
const { getServerSideProps } = matchedComponent[0].route.element.type;
if (getServerSideProps) {
const res = getServerSideProps();
reduxState = { ...reduxState, ...res };
}
}

const store = configureStore(reduxState);

// renderMatches is an API provided by react-router for rendering the results of matchRoutes,
// where the content is a component injected with Redux data, representing the hydration and dehydration process
const content = renderToString(<Provider store={store}>{renderMatches(matches)}</Provider>);

// Concatenate the content into the pre-set HTML template
const response = template(store.getState(), content);

// Return the HTML to the user
res.setHeader('Cache-Control', 'assets, max-age=604800');
res.send(response);
});

Client-side

Hydration

Here is the code for the above-mentioned template function:

function template(initialState = {}, content = '') {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="/assets/style.css">
</head>
<body>
<div id="app">
${content}
</div>

<script>
window.__STATE__ = ${JSON.stringify(initialState)}
</script>
<script src="/assets/bundle/client.js"></script>
</body>
</html>
`;
}

Hydration process:

import React from 'react';
import { hydrate } from 'react-dom';
import { Provider } from 'react-redux';
import configureStore from './redux/configureStore';
import { BrowserRouter } from 'react-router-dom';
import App from './App';

const state = window.__STATE__;
delete window.__STATE__;

// Initialize the store to maintain consistency between the client and the server
const store = configureStore(state);

hydrate(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.querySelector('#app')
);

Component Syntax

import axios from 'axios';
import React from 'react';
import { Helmet } from 'react-helmet';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';

const Detail = () => {
const { name } = useParams();
const serverData = useSelector(state => state.serverData);

return (
<div>
<Helmet>
<title>{name}</title>
</Helmet>
<div>{name}</div>

<h4>
<strong style={{ color: 'red' }}>{serverData}</strong>{' '}
</h4>
</div>
);
};

Detail.getServerSideProps = () => {
return {
serverData: 'content init from serverSide'
};
};

export default Detail;

Summary

In simple terms, the server-side finds the component to be rendered based on the URL, injects the data returned from getServerSideProps into the component, and then uses renderToString to render and send it to the client-side.

The client-side performs hydration, during which React tries to reuse the HTML structure as much as possible. React initializes based on the HTML structure returned from the server.

This article is licensed under the CC BY-NC-SA 4.0.