Guides / Building Search UI / Going further / Server-side rendering

Server-Side Rendering with React InstantSearch Hooks

Server-side rendering (SSR) lets you generate HTML from InstantSearch components on the server.

Integrating SSR with React InstantSearch Hooks:

  • Improves general performance: the browser directly loads with HTML containing search results, and React preserves the existing markup (hydration) instead of re-rendering everything.
  • Improves perceived performance: users don’t see a UI flash when loading the page, but directly the search UI. This can also positively impact your Largest Contentful Paint score.
  • Improves SEO: the content is accessible to any search engine, even those that don’t execute JavaScript.

Here’s the SSR flow for InstantSearch:

  1. On the server, retrieve the initial search results of the current search state.
  2. Then, on the server, render these search results to HTML and send the response to the browser.
  3. Then, on the browser, load the JavaScript code for InstantSearch.
  4. Then, on the browser, hydrate the server-side rendered InstantSearch application.

React InstantSearch Hooks is compatible with server-side rendering. The library provides an API that works with any SSR solution.

Install the server package

The InstantSearch server APIs are available from the companion react-instantsearch-hooks-server package.

1
2
3
yarn add react-instantsearch-hooks-server
# or
npm install react-instantsearch-hooks-server

With a custom server

This guide shows how to server-side render your application with an express server. You can follow the same approach with any Node.js server.

There are 3 different files:

  • App.js: the React component shared between the server and the browser
  • server.js: the server entry to a Node.js HTTP server
  • browser.js: the browser entry (which gets compiled to assets/bundle.js)

Create the React component

App.js is the main entry point to your React application. It exports an <App> component that you can render both on the server and in the browser.

The <InstantSearchSSRProvider> component receives the server state and forwards it to <InstantSearch>.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import algoliasearch from 'algoliasearch/lite';
import React from 'react';
import {
  InstantSearch,
  InstantSearchSSRProvider,
} from 'react-instantsearch-hooks-web';

const searchClient = algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey');

function App({ serverState }) {
  return (
    <InstantSearchSSRProvider {...serverState}>
      <InstantSearch indexName="instant_search" searchClient={searchClient}>
        {/* Widgets */}
      </InstantSearch>
    </InstantSearchSSRProvider>
  );
}

export default App;

Server-render the page

When you receive the request on the server, you need to retrieve the server state so you can pass it down to <App>. This is what getServerState() does: it receives your InstantSearch application and computes a search state from it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { getServerState } from 'react-instantsearch-hooks-server';
import App from './App';

const app = express();

app.get('/', async (req, res) => {
  const serverState = await getServerState(<App />);
  const html = renderToString(<App serverState={serverState} />);

  res.send(
    `
  <!DOCTYPE html>
  <html>
    <head>
      <script>window.__SERVER_STATE__ = ${JSON.stringify(serverState)};</script>
    </head>
    <body>
      <div id="root">${html}</div>
    </body>
    <script src="/assets/bundle.js"></script>
  </html>
    `
  );
});

app.listen(8080);

Here, the server:

  • Retrieves the server state with getServerState().
  • Then, renders the <App> as HTML with this server state.
  • Then, sends the HTML to the browser.

Since you’re sending plain HTML to the browser, you need a way to forward the server state object so you can reuse it in your InstantSearch application. To do so, you can serialize it and store it on the window object (here on the __SERVER_STATE__ global), for later reuse in browser.js.

Hydrate the app in the browser

Once the browser has received HTML from the server, the final step is to connect this markup to the interactive application. This step is called hydration.

1
2
3
4
5
6
7
8
9
10
import React from 'react';
import { hydrate } from 'react-dom';
import App from './App';

hydrate(
  <App serverState={window.__SERVER_STATE__} />,
  document.querySelector('#root')
);

delete window.__SERVER_STATE__;

Deleting __SERVER_STATE__ from the global object allows the server state to be garbage collected.

Support routing

Server-side rendered search experiences should be able to generate HTML based on the current URL. You can use the history router to synchronize <InstantSearch> with the browser URL.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import algoliasearch from 'algoliasearch/lite';
import { history } from 'instantsearch.js/es/lib/routers';
import React from 'react';
import {
  InstantSearch,
  InstantSearchSSRProvider,
} from 'react-instantsearch-hooks-web';

const searchClient = algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey');

function App({ serverState, location }) {
  return (
    <InstantSearchSSRProvider {...serverState}>
      <InstantSearch
        indexName="instant_search"
        searchClient={searchClient}
        routing={{
          router: history({
            getLocation: () =>
              typeof window === 'undefined' ? location : window.location,
          }),
        }}
      >
        {/* Widgets */}
      </InstantSearch>
    </InstantSearchSSRProvider>
  );
}

export default App;

You can rely on window.location when rendering in the browser, and use the location provided by the server when rendering on the server.

On the server, you need to recreate the URL and to pass it to the <App>:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { getServerState } from 'react-instantsearch-hooks-server';
import App from './App';

const app = express();

app.get('/', async (req, res) => {
  const location = new URL(
    `${req.protocol}://${req.get('host')}${req.originalUrl}`
  );
  const serverState = await getServerState(<App location={location} />);
  const html = renderToString(<App location={location} />);

  res.send(
    `
  <!DOCTYPE html>
  <html>
    <head>
      <script>window.__SERVER_STATE__ = ${JSON.stringify(serverState)};</script>
    </head>
    <body>
      <div id="root">${html}</div>
    </body>
    <script src="/assets/bundle.js"></script>
  </html>
    `
  );
});

app.listen(8080);

Check the complete SSR example with express.

With Next.js

Next.js is a React framework that abstracts the redundant and complicated parts of SSR. Server-side rendering an InstantSearch application is easier with Next.js.

Server-side rendering a page in Next.js is split in two parts: a function that returns data from the server, and a React component for the page that receives this data.

In the page, you need to wrap the search experience with the <InstantSearchSSRProvider> component. This provider receives the server state and forwards it to the entire InstantSearch application.

In Next’s getServerSideProps(), you can use getServerState() to return the server state as a prop. To support routing, you can forward the server’s request URL to the history router.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import algoliasearch from 'algoliasearch/lite';
import {
  InstantSearch,
  InstantSearchSSRProvider,
} from 'react-instantsearch-hooks-web';
import { getServerState } from 'react-instantsearch-hooks-server';
import { history } from 'instantsearch.js/es/lib/routers/index.js';

const searchClient = algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey');

export default function SearchPage({ serverState, url }) {
  return (
    <InstantSearchSSRProvider {...serverState}>
      <InstantSearch
        searchClient={searchClient}
        indexName="instant_search"
        routing={{
          router: history({
            getLocation: () =>
              typeof window === 'undefined' ? new URL(url) : window.location,
          }),
        }}
      >
        {/* Widgets */}
      </InstantSearch>
    </InstantSearchSSRProvider>
  );
}

export async function getServerSideProps({ req }) {
  const protocol = req.headers.referer?.split('://')[0] || 'https';
  const url = `${protocol}://${req.headers.host}${req.url}`;
  const serverState = await getServerState(<SearchPage url={url} />);

  return {
    props: {
      serverState,
      url,
    },
  };
}

Check the complete SSR example with Next.js.

Did you find this page helpful?