Implement server-side rendering with React
Implement server-side rendering with React
Server-side rendering (SSR) is an important consideration for sites where search engine optimization (SEO) and fast perceived load times are important. These attributes are critical for successful e-commerce deployments. This article guides you through the implementation of SSR for an application built on Headless and React.
Create a Server
npm i express
Next, create a server listening on port 3000
.
Also, make sure to serve static files from the build
directory.
Applications bootstrapped with create-react-app
use this location by default.
If this is not the case for your application, make sure to adjust it.
// server.jsx
const express = require('express');
const PORT = 3000;
const app = express();
app.get('/', async (req, res) => {
// ...
});
app.use(express.static('./build'));
app.listen(PORT, () => {
console.log(`Server is listening on port ${PORT}`);
});
Create an Engine Instance and Perform the Initial Rendering
When the server receives a request on the /
endpoint, create a headless engine instance.
Next, pass the engine instance as a prop to the React application, and perform a first render with the help of the ReactDOMServer.renderToString
function.
This initial render allows React components to initialize their Headless controllers, which in turn updates the engine state as needed to perform the first search request.
|
Note
If you use class-based components, it’s important to initialize controllers inside the constructor.
The |
//server.jsx
// ...
const {buildSearchEngine} = require('@coveo/headless');
const ReactDOMServer = require('react-dom/server');
const App = require('../src/App');
app.get('/', async (req, res) => {
const engine = buildSearchEngine({
configuration: {
organizationId: '<ORGANIZATION_ID>',
accessToken: '<ACCESS_TOKEN>'
},
});
renderServerSide(engine);
});
function renderServerSide(engine) {
return ReactDOMServer.renderToString(<App engine={engine} />);
}
The root component, which we use to perform the initial render at the end of this sample. |
Perform the First Search
Once the initial render is complete, the Headless engine has the search request ready and waiting in state.
Headless provides a buildSearchStatus
controller to determine when the first search is complete.
Inside a promise, subscribe to the controller and then trigger a search:
//server.jsx
const {buildSearchEngine, buildSearchStatus} = require('@coveo/headless');
//...
app.get('/', async (req, res) => {
const engine = buildSearchEngine({
configuration: {
// ...
},
});
renderServerSide(engine);
await firstSearchExecuted(engine);
});
function firstSearchExecuted(engine) {
return new Promise((resolve) => {
const searchStatus = buildSearchStatus(engine);
searchStatus.subscribe(
() => searchStatus.state.firstSearchExecuted && resolve(true)
);
engine.executeFirstSearch();
});
}
Perform the Second Render
Once the first search request is complete, the Headless engine holds the result items and facet values in state.
Call renderToString
a second time so that they render correctly.
You can reuse the same function as the one you used in your first render, but this time store the result in a variable:
// server.jsx
// ...
app.get('/', async (req, res) => {
const engine = buildSearchEngine({
configuration: {
// ...
},
});
renderServerSide(engine);
await firstSearchExecuted(engine);
const app = renderServerSide(engine);
});
Embed the Server-Side Render and Engine State in the Root HTML File
You now have a string representation of the application in the app
variable.
The next step is to embed it in the HTML file that the /
endpoint responds with.
Do so by leveraging a placeholder inside the HTML file and performing a string replace.
Do the same with the Headless engine state after converting it to JSON, and storing the string in a variable called HEADLESS_STATE
on the global window
object.
It will become clear why this is important in the next step.
The endpoint concludes by returning the assembled HTML file as the response.
Say that the /
endpoint responds with the following HTML file:
<!-- ./build/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<!-- ... -->
<script id="ssr"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
Your server.jsx
file would now look as follows:
const fs = require('fs');
//...
app.get('/', async (req, res) => {
const engine = buildSearchEngine({
configuration: {
// ...
},
});
renderServerSide(engine);
await firstSearchExecuted(engine);
const app = renderServerSide(engine);
const indexFile = path.resolve('./build/index.html');
fs.readFile(indexFile, 'utf8', (err, data) => {
if (err) {
console.error('Something went wrong:', err);
return res.status(500).send('Internal error');
}
const state = JSON.stringify(engine.state);
const page = data
.replace('<div id="root"></div>', `<div id="root">${app}</div>`)
.replace(
'<script id="ssr"></script>',
`<script id="ssr">window.HEADLESS_STATE = ${state}</script>`
);
return res.send(page);
});
});
Hydrate the Application Client-Side
When a browser first loads a SSR response, the application isn’t interactive. The browser needs to re-run the JavaScript code that was just run on the server, and then reconcile the rendered HTML with the application state. This process is called hydration.
In the browser, initialize the application using the React hydrate
method:
// index.jsx
// ...
ReactDOM.hydrate(<App/>, document.getElementById('root'))
In the root component, i.e., App
, access the Headless state in the window.HEADLESS_STATE
variable and pass it as the preloadedState
parameter (see SearchEngineOptions
).
// App.jsx
// ...
function App(props) {
let engine;
if (props.engine) {
engine = props.engine
} else {
engine = buildSearchEngine({
configuration: {
// ...
},
preloadedState: window.HEADLESS_STATE,
});
}
return <SearchPage engine={engine}/>
}
Recall from the second step that you pass the engine as a prop to the root component of your app when rendering server-side. | |
Since we don’t pass an engine as prop when hydrating the app from the browser, it means we are client-side here.
Accordingly, create a new engine, setting the preloadedState parameter to the value of window.HEADLESS_STATE . |
Log the First Analytics Event
Finally, you need to log a usage analytics event to mark the start of the user session.
Do this client side, to allow Headless to leverage visitor information that could be present in the browser local storage.
For a search page, the event to dispatch is typically logInterfaceLoad
:
// App.jsx
// ...
function App(props) {
let engine;
if (props.engine) {
// ...
} else {
engine = buildSearchEngine({
configuration: {
// ...
},
preloadedState: window.HEADLESS_STATE,
});
const {logInterfaceLoad} = loadSearchAnalyticsActions(this.engine);
this.engine.dispatch(logInterfaceLoad());
}
return <SearchPage engine={engine}/>
}
Congratulations, your application now renders server-side! Search engines like Google will better understand your site when crawling it, which in turn will allow them to better recommend your site. Visitors will also see your site load faster, especially when on less powerful mobile devices or slower internet connections.
Don’t hesitate to have a look at a complete example in the following files: