Server-side rendering (SSR) in InstantSearch pre-renders search results on the server, improving initial page load performance and SEO. The server generates HTML with search results, which is then hydrated on the client for interactivity.
Why use SSR?
Server-side rendering provides several benefits:
SEO optimization Search engines can index your search results, improving discoverability.
Faster initial load Users see content immediately without waiting for JavaScript to load and execute.
Better perceived performance Content appears faster even if total load time is similar to client-side rendering.
Social media previews Link previews on social platforms show actual search results.
How SSR works in InstantSearch
Server: Get server state
Render your InstantSearch app on the server to collect search requests and fetch results.
Server: Render with results
Render the app again with the fetched results to generate HTML.
Client: Hydrate
Send the server state to the client and hydrate the InstantSearch instance with the cached results.
Client: Interactive
Once hydrated, the client takes over and new searches happen on the client side.
React with Next.js (App Router)
The simplest way to use SSR is with Next.js App Router and InstantSearchNext:
// app/search/page.tsx
import { liteClient as algoliasearch } from 'algoliasearch/lite' ;
import { InstantSearchNext } from 'react-instantsearch-nextjs' ;
import { SearchBox , Hits , RefinementList } from 'react-instantsearch' ;
const searchClient = algoliasearch ( 'YourAppID' , 'YourSearchKey' );
export default function Search () {
return (
< InstantSearchNext
searchClient = { searchClient }
indexName = "products"
routing
>
< SearchBox />
< RefinementList attribute = "brand" />
< Hits />
</ InstantSearchNext >
);
}
InstantSearchNext automatically handles SSR, hydration, and routing in Next.js App Router. No additional configuration needed!
React with Next.js (Pages Router)
For Next.js Pages Router, use getServerState with InstantSearchSSRProvider:
// pages/search.tsx
import { GetServerSideProps } from 'next' ;
import { renderToString } from 'react-dom/server' ;
import { liteClient as algoliasearch } from 'algoliasearch/lite' ;
import {
InstantSearch ,
InstantSearchSSRProvider ,
getServerState ,
SearchBox ,
Hits ,
RefinementList ,
} from 'react-instantsearch' ;
const searchClient = algoliasearch ( 'YourAppID' , 'YourSearchKey' );
type SearchPageProps = {
serverState ?: any ;
url ?: string ;
};
export default function SearchPage ({ serverState , url } : SearchPageProps ) {
return (
< InstantSearchSSRProvider { ... serverState } >
< InstantSearch
searchClient = { searchClient }
indexName = "products"
routing = { { router: { createURL : () => url || '' } } }
>
< SearchBox />
< RefinementList attribute = "brand" />
< Hits />
</ InstantSearch >
</ InstantSearchSSRProvider >
);
}
export const getServerSideProps : GetServerSideProps < SearchPageProps > = async ({
req ,
}) => {
const protocol = req . headers . referer ?. split ( '://' )[ 0 ] || 'https' ;
const url = ` ${ protocol } :// ${ req . headers . host }${ req . url } ` ;
const serverState = await getServerState (
< SearchPage url = { url } /> ,
{ renderToString }
);
return {
props: {
serverState ,
url ,
},
};
};
How it works
getServerSideProps
Next.js calls this on the server for each request.
getServerState
Renders your app, collects search requests, fetches results, and returns serialized state.
InstantSearchSSRProvider
Provides the server state to the client InstantSearch instance.
Hydration
Client renders with cached results, avoiding duplicate requests.
getServerState API
The getServerState function is the core of SSR (based on /home/daytona/workspace/source/packages/react-instantsearch-core/src/components/InstantSearchSSRProvider.tsx:9-11):
type InstantSearchServerState = {
initialResults : InitialResults ;
};
function getServerState (
children : ReactElement ,
options : { renderToString : ( element : ReactElement ) => string }
) : Promise < InstantSearchServerState >
Usage:
import { renderToString } from 'react-dom/server' ;
import { getServerState } from 'react-instantsearch' ;
const serverState = await getServerState (
< App /> ,
{ renderToString }
);
InstantSearchSSRProvider API
Provides server state to the client (from /home/daytona/workspace/source/packages/react-instantsearch-core/src/components/InstantSearchSSRProvider.tsx:18-26):
type InstantSearchSSRProviderProps = Partial < InstantSearchServerState > & {
children ?: ReactNode ;
};
function InstantSearchSSRProvider ( props : InstantSearchSSRProviderProps )
Usage:
< InstantSearchSSRProvider { ... serverState } >
< InstantSearch searchClient = { searchClient } indexName = "products" >
{ /* widgets */ }
</ InstantSearch >
</ InstantSearchSSRProvider >
React with other SSR frameworks
Remix
// routes/search.tsx
import { json , LoaderFunction } from '@remix-run/node' ;
import { useLoaderData } from '@remix-run/react' ;
import { renderToString } from 'react-dom/server' ;
import {
InstantSearch ,
InstantSearchSSRProvider ,
getServerState ,
} from 'react-instantsearch' ;
export const loader : LoaderFunction = async ({ request }) => {
const url = new URL ( request . url );
const serverState = await getServerState (
< SearchPage url = { url . toString () } /> ,
{ renderToString }
);
return json ({ serverState , url: url . toString () });
};
export default function Search () {
const { serverState , url } = useLoaderData < typeof loader >();
return < SearchPage serverState = { serverState } url = { url } /> ;
}
function SearchPage ({ serverState , url }) {
return (
< InstantSearchSSRProvider { ... serverState } >
< InstantSearch
searchClient = { searchClient }
indexName = "products"
routing = { { router: { createURL : () => url } } }
>
< SearchBox />
< Hits />
</ InstantSearch >
</ InstantSearchSSRProvider >
);
}
Express
import express from 'express' ;
import { renderToString } from 'react-dom/server' ;
import { getServerState } from 'react-instantsearch' ;
const app = express ();
app . get ( '/search' , async ( req , res ) => {
const url = ` ${ req . protocol } :// ${ req . get ( 'host' ) }${ req . originalUrl } ` ;
const serverState = await getServerState (
< App url = { url } /> ,
{ renderToString }
);
const html = renderToString (
< InstantSearchSSRProvider { ... serverState } >
< App url = { url } />
</ InstantSearchSSRProvider >
);
res . send ( `
<!DOCTYPE html>
<html>
<body>
<div id="root"> ${ html } </div>
<script>
window.__SERVER_STATE__ = ${ JSON . stringify ( serverState ) } ;
</script>
<script src="/bundle.js"></script>
</body>
</html>
` );
});
Vue SSR
For Vue, use the standard InstantSearch instance with initial results:
// server.js
import { renderToString } from '@vue/server-renderer' ;
import { createSSRApp } from 'vue' ;
import InstantSearch from 'vue-instantsearch/vue3/es' ;
const app = createSSRApp ({
// Your app component
});
app . use ( InstantSearch );
// Render on server
const html = await renderToString ( app );
The server state contains initial results in this format:
type InitialResults = {
[ indexId : string ] : {
// Search results
results ?: SearchResults [];
// Recommendation results
recommend ?: {
results : RecommendResponse [];
};
};
};
Example:
{
products : {
results : [
{
hits: [ ... ],
nbHits: 1000 ,
page: 0 ,
nbPages: 50 ,
// ... other result properties
}
]
}
}
Handling routing with SSR
When using routing with SSR, pass the current URL:
import { history } from 'instantsearch.js/es/lib/routers' ;
function SearchPage ({ url }) {
const routing = {
router: history ({
createURL ({ qsModule , routeState }) {
// Generate URLs using the server URL
const queryString = qsModule . stringify ( routeState );
return ` ${ url . split ( '?' )[ 0 ] } ? ${ queryString } ` ;
},
}),
};
return (
< InstantSearch
searchClient = { searchClient }
indexName = "products"
routing = { routing }
>
{ /* widgets */ }
</ InstantSearch >
);
}
Cache search client
Create the search client once and reuse it:
// searchClient.js
import { liteClient as algoliasearch } from 'algoliasearch/lite' ;
export const searchClient = algoliasearch (
process . env . ALGOLIA_APP_ID ,
process . env . ALGOLIA_SEARCH_KEY
);
Use search client cache
The search client caches requests automatically. Ensure you’re using the same client instance:
// Server and client use same instance
import { searchClient } from './searchClient' ;
Limit initial results
Fetch fewer results on the server:
< Configure hitsPerPage = { 10 } />
Deduplicate requests
InstantSearch automatically deduplicates identical requests during SSR.
Common SSR patterns
Conditional SSR
Only use SSR for certain routes:
export const getServerSideProps : GetServerSideProps = async ({ query }) => {
// Only SSR if there are search parameters
if ( Object . keys ( query ). length === 0 ) {
return { props: {} };
}
const serverState = await getServerState (
< SearchPage /> ,
{ renderToString }
);
return { props: { serverState } };
};
SSR with authentication
Use secured API keys based on user:
export const getServerSideProps : GetServerSideProps = async ({ req }) => {
const user = await getUser ( req );
const searchClient = algoliasearch (
'YourAppID' ,
generateSecuredApiKey ( user )
);
const serverState = await getServerState (
< SearchPage searchClient = { searchClient } /> ,
{ renderToString }
);
return { props: { serverState } };
};
Progressive hydration
Hydrate only critical widgets first:
< InstantSearch searchClient = { searchClient } indexName = "products" >
{ /* Always rendered */ }
< SearchBox />
< Hits />
{ /* Lazy loaded */ }
{ isClient && (
<>
< RefinementList attribute = "brand" />
< Pagination />
</>
) }
</ InstantSearch >
Debugging SSR
Check server state
const serverState = await getServerState (< App />, { renderToString });
console . log ( 'Server state:' , JSON . stringify ( serverState , null , 2 ));
Verify hydration
React will warn about hydration mismatches in the console. Common causes:
Different state on server and client
Missing InstantSearchSSRProvider
Incorrect URL passed to routing
Non-deterministic rendering (random values, dates, etc.)
Compare server and client HTML
// Server
const serverHtml = renderToString (
< InstantSearchSSRProvider { ... serverState } >
< App />
</ InstantSearchSSRProvider >
);
console . log ( 'Server HTML:' , serverHtml );
// Client should produce identical HTML on first render
Best practices
Use InstantSearchNext for Next.js The InstantSearchNext component handles all SSR complexity automatically in Next.js App Router.
Cache the search client Create a single search client instance and reuse it on server and client.
Pass consistent URLs Ensure the URL passed to routing is the same on server and client to avoid hydration issues.
Handle loading states Show loading indicators during hydration for a better user experience.
Limit initial requests Use Configure to limit results and reduce server response time.
Monitor performance Track server response times and optimize slow SSR renders.
Limitations
DynamicWidgets : The DynamicWidgets component requires a two-pass render to discover attributes. This works automatically but doubles the server rendering time.
Recommendation widgets : Recommendation widgets (RelatedProducts, TrendingItems, etc.) are supported but add additional requests during SSR.
Browser-only features : Some features like geolocation-based search may not work during SSR and need client-side initialization.
SSR vs Static Site Generation (SSG)
Use SSR when:
Content changes frequently
User-specific results (authentication)
Real-time data requirements
Personalized search
Use SSG when:
Content is mostly static
Same results for all users
Build-time generation is acceptable
Maximum performance needed
For Next.js, use getStaticProps instead of getServerSideProps for SSG:
export const getStaticProps : GetStaticProps = async () => {
const serverState = await getServerState (
< SearchPage /> ,
{ renderToString }
);
return {
props: { serverState },
revalidate: 3600 , // Regenerate every hour
};
};
Troubleshooting
Hydration mismatch errors
Cause : Server and client render differently.Solution : Ensure:
Same search client on server and client
Correct URL passed to routing
No browser-only code in initial render
InstantSearchSSRProvider wraps your app
Duplicate requests on client
Cause : Server state not properly hydrated.Solution : Verify:
InstantSearchSSRProvider has serverState prop
Server state is serialized correctly
No unmount/remount of InstantSearch
Cause : Search client not configured correctly.Solution : Check:
API credentials are accessible on server
Search client is created before getServerState
Network requests are allowed from server
Cause : Too many widgets or large result sets.Solution :
Reduce hitsPerPage
Limit number of widgets
Use caching
Consider SSG instead
Routing URL synchronization with SSR
Search state Understanding state structure
Next.js guide Complete Next.js integration guide
InstantSearch instance Core instance configuration