Server-side rendering with React InstantSearch
This is the React InstantSearch v7 documentation. React InstantSearch v7 is the latest version of React InstantSearch and the stable version of React InstantSearch Hooks.
If you were using React InstantSearch v6, you can upgrade to v7.
If you were using React InstantSearch Hooks, you can still use the React InstantSearch v7 documentation, but you should check the upgrade guide for necessary changes.
If you want to keep using React InstantSearch v6, you can find the archived documentation.
On this page
Server-side rendering (SSR) lets you generate HTML from InstantSearch components on the server.
Integrating SSR with React InstantSearch:
- 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:
- On the server, retrieve the initial search results of the current search state.
- Then, on the server, render these search results to HTML and send the response to the browser.
- Then, on the browser, load the JavaScript code for InstantSearch.
- Then, on the browser, hydrate the server-side rendered InstantSearch app.
React InstantSearch is compatible with server-side rendering. The library provides an API that works with any SSR solution.
With a custom server
This guide shows how to server-side render your app with an express server. You can follow the same approach with any Node.js server.
The example in this guide has three files:
App.js
: the React component shared between the server and the browserserver.js
: the server entry to a Node.js HTTP serverbrowser.js
: the browser entry (which gets compiled toassets/bundle.js
)
Create the React component
App.js
is the main entry point to your React app. 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';
const searchClient = algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey');
function App({ serverState }) {
return (
<InstantSearchSSRProvider {...serverState}>
<InstantSearch indexName="YourIndexName" 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 app 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';
import App from './App';
const app = express();
app.get('/', async (req, res) => {
const serverState = await getServerState(<App />, { renderToString });
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 app. 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 app. 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
31
32
import algoliasearch from 'algoliasearch/lite';
import { history } from 'instantsearch.js/es/lib/routers';
// or cjs if you're running in a CommonJS environment
// import { history } from 'instantsearch.js/cjs/lib/routers';
import React from 'react';
import {
InstantSearch,
InstantSearchSSRProvider,
} from 'react-instantsearch';
const searchClient = algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey');
function App({ serverState, location }) {
return (
<InstantSearchSSRProvider {...serverState}>
<InstantSearch
indexName="YourIndexName"
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
33
34
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { getServerState } from 'react-instantsearch';
import App from './App';
const app = express();
app.get('/', async (req, res) => {
const serverUrl = new URL(
`${req.protocol}://${req.get('host')}${req.originalUrl}`
);
const serverState = await getServerState(<App serverUrl={serverUrl} />, {
renderToString,
});
const html = renderToString(<App serverUrl={serverUrl} />);
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 app is easier with Next.js.
Pages router
For App Router support, see App Router (experimental).
Server-side rendering a page with the Pages Router 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.
On the page, wrap the search experience with the <InstantSearchSSRProvider>
component. This provider receives the server state and forwards it to the entire InstantSearch app.
Server-side rendering
In Next’s getServerSideProps()
, you can use getServerState()
to return the server state as a prop. To support routing, you can use the createInstantSearchRouterNext()
function from the react-instantsearch-router-nextjs
package.
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
42
43
44
import { renderToString } from 'react-dom/server';
import algoliasearch from 'algoliasearch/lite';
import {
InstantSearch,
InstantSearchSSRProvider,
getServerState,
} from 'react-instantsearch';
import { history } from 'instantsearch.js/es/lib/routers/index.js';
import singletonRouter from 'next/router';
import { createInstantSearchRouterNext } from 'react-instantsearch-router-nextjs';
const searchClient = algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey');
export default function SearchPage({ serverState, serverUrl }) {
return (
<InstantSearchSSRProvider {...serverState}>
<InstantSearch
searchClient={searchClient}
indexName="YourIndexName"
routing={{
router: createInstantSearchRouterNext({ singletonRouter, serverUrl }),
}}
>
{/* Widgets */}
</InstantSearch>
</InstantSearchSSRProvider>
);
}
export async function getServerSideProps({ req }) {
const protocol = req.headers.referer?.split('://')[0] || 'https';
const serverUrl = `${protocol}://${req.headers.host}${req.url}`;
const serverState = await getServerState(
<SearchPage serverUrl={serverUrl} />,
{ renderToString }
);
return {
props: {
serverState,
serverUrl,
},
};
}
Check the complete SSR example with Next.js.
Static site generation
You can generate a static version of your search page at build time using Next’s getStaticProps()
. Static site generation (or pre-rendering) is essentially the same thing as server-side rendering, except the latter happens at request time, while the former happens at build time.
You can use the same <InstantSearchSSRProvider>
and getServerState()
APIs for both server-side rendering and static site generation.
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
import { renderToString } from 'react-dom/server';
import algoliasearch from 'algoliasearch/lite';
import {
InstantSearch,
InstantSearchSSRProvider,
getServerState,
} from 'react-instantsearch';
const searchClient = algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey');
export default function SearchPage({ serverState }) {
return (
<InstantSearchSSRProvider {...serverState}>
<InstantSearch searchClient={searchClient} indexName="YourIndexName">
{/* Widgets */}
</InstantSearch>
</InstantSearchSSRProvider>
);
}
export async function getStaticProps() {
const serverState = await getServerState(<SearchPage />, { renderToString });
return {
props: {
serverState,
},
};
}
Dynamic routes
If you want to generate pages dynamically—for example, one for each brand—you can use Next’s getStaticPaths()
API.
The following example uses dynamic routes along with getStaticPaths()
to create one page per brand.
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
42
43
44
45
46
47
48
49
50
51
52
53
import { renderToString } from 'react-dom/server';
import algoliasearch from 'algoliasearch/lite';
import {
InstantSearch,
InstantSearchSSRProvider,
SearchBox,
Hits,
Configure,
getServerState,
} from 'react-instantsearch';
const searchClient = algoliasearch(
'YourApplicationID',
'YourSearchOnlyAPIKey'
);
export default function BrandPage({ brand, serverState }) {
return (
<InstantSearchSSRProvider {...serverState}>
<InstantSearch searchClient={searchClient} indexName="YourIndexName">
<Configure facetFilters={`brand:${brand}`} />
<SearchBox />
<Hits />
</InstantSearch>
</InstantSearchSSRProvider>
);
}
export async function getStaticPaths() {
return {
// You can retrieve your brands from an API, a database, a file, etc.
paths: [{ params: { brand: 'Apple' } }, { params: { brand: 'Samsung' } }],
fallback: 'blocking', // or `true` or `false`
};
}
export async function getStaticProps({ params }) {
if (!params) {
return { notFound: true };
}
const serverState = await getServerState(
<BrandPage brand={params.brand} />,
{ renderToString }
);
return {
props: {
brand: params.brand,
serverState,
},
};
}
If you have a reasonable amount of paths to generate and this number doesn’t change much, you can generate them all at build time. In this case, you can set fallback: false
, which will serve a 404 page to users who try to visit a path that doesn’t exist (for example, a brand that isn’t in your dataset).
If there are many categories and generating them all significantly slows down your build, you can pre-render only a subset of them (for example, the most popular ones) and generate the rest on the fly.
With fallback: true
, whenever a user visits a path that doesn’t exist, your getStaticProps()
code runs on the server and the page is generated once for all subsequent users. The user sees a loading screen that you can implement with router.isFallback
until the page is ready.
With fallback: 'blocking'
, the scenario is the same as with fallback: true
but there’s no loading screen. The server only returns the HTML once the page is generated.
App router (experimental)
App router support is an experimental feature. It may break in the future, so make sure to pin your dependency versions if you’re using it in production.
As of Next.js 13, you can use the App Router to structure your app. The App Router has a different approach to data fetching than the Pages Router, which changes the approach to server-side rendering.
To support server-side rendering for the App Router, use the react-instantsearch-nextjs
package. It provides an <InstantSearchNext>
component that replaces your <InstantSearch>
component.
Install react-instantsearch-nextjs
First, make sure you have the correct dependencies installed:
react-instantsearch
>=7.1.0
next
>=13.14.0
Then, install the react-instantsearch-nextjs
package:
$
$
$
yarn add react-instantsearch-nextjs
# or
npm install react-instantsearch-nextjs
Usage
Your search component must be in its own file, and it shouldn’t be named page.js
or page.tsx
.
To render the component in the browser and allow users to interact with it, include the “use client” directive at the top of your code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
+'use client';
import algoliasearch from 'algoliasearch/lite';
import {
InstantSearch,
SearchBox,
} from 'react-instantsearch';
const searchClient = algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey');
export function Search() {
return (
<InstantSearch indexName="YourIndexName" searchClient={searchClient}>
<SearchBox />
{/* other widgets */}
</InstantSearch>
);
}
Import the <InstantSearchNext>
component from the react-instantsearch-nextjs
package, and replace the <InstantSearch>
component with it, without changing the props.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
'use client';
import algoliasearch from 'algoliasearch/lite';
import {
- InstantSearch,
SearchBox,
} from 'react-instantsearch';
+import { InstantSearchNext } from 'react-instantsearch-nextjs';
const searchClient = algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey');
export function Search() {
return (
- <InstantSearch indexName="YourIndexName" searchClient={searchClient}>
+ <InstantSearchNext indexName="YourIndexName" searchClient={searchClient}>
<SearchBox />
{/* other widgets */}
- </InstantSearch>
+ </InstantSearchNext>
);
}
To serve your search page at /search
, create an app/search
directory. Inside it, create a page.js
file (or page.tsx
if you’re using TypeScript).
Make sure to configure your route segment to be dynamic so that Next.js generates a new page for each request.
1
2
3
4
5
6
7
8
// app/search/page.js or app/search/page.tsx
import { Search } from './Search'; // change this with the path to your <Search> component
export const dynamic = 'force-dynamic';
export default function Page() {
return <Search />;
}
You can now visit /search
to see your server-side rendered search page.
If you were previously using getServerState()
in getServerSideProps()
with the Pages Router,
remove any references to it. It’s not needed with the App Router.
Enabling routing
To enable routing, add a boolean routing
prop to <InstantSearchNext>
.
1
2
3
4
5
6
7
8
9
10
11
function Search() {
return (
<InstantSearchNext
indexName="YourIndexName"
searchClient={searchClient}
+ routing
>
{/* widgets */}
</InstantSearchNext>
);
}
To customize the way InstantSearch maps the state to the route, pass an object to routing
, where router
has the same options as history
, and stateMapping
has the same options as the one in <InstantSearch>
.
With Remix
Remix is a full-stack web framework that encourages usage of runtime servers, notably for server-side rendering.
Server-side rendering a page in Remix is split in two parts: a loader 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 app.
In Remix’ loader, you can use getServerState()
to return the server state. 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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import { renderToString } from 'react-dom/server';
import algoliasearch from 'algoliasearch/lite';
import {
InstantSearch,
InstantSearchSSRProvider,
getServerState,
} from 'react-instantsearch';
import { history } from 'instantsearch.js/cjs/lib/routers/index.js';
import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
const searchClient = algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey');
export async function loader({ request }) {
const serverUrl = request.url;
const serverState = await getServerState(
<Search serverUrl={serverUrl} />,
{ renderToString }
);
return json({
serverState,
serverUrl,
});
}
function Search({ serverState, serverUrl }) {
return (
<InstantSearchSSRProvider {...serverState}>
<InstantSearch
searchClient={searchClient}
indexName="YourIndexName"
routing={{
router: history({
getLocation() {
if (typeof window === 'undefined') {
return new URL(serverUrl);
}
return window.location;
},
}),
}}
>
{/* Widgets */}
</InstantSearch>
</InstantSearchSSRProvider>
);
}
export default function HomePage() {
const { serverState, serverUrl } = useLoaderData();
return <Search serverState={serverState} serverUrl={serverUrl} />;
}
Check the complete SSR example with Remix.