We recommend using the Coveo Headless SSR utilities with the latest Next.js App Router. We don't fully support the Pages Router and using it may result in unexpected behavior. This article uses the App Router paradigm.
Notes
The strategy for implementing SSR in Headless is as follows:
StaticStateProvider and render it.StaticStateProvider with a HydratedStateProvider and render it.Create and export an engine definition in a shared file. It should include the controllers, their settings, and the search engine configuration, as in the following example:
import {
defineSearchEngine,
defineSearchBox,
defineResultList,
defineFacet,
getSampleSearchEngineConfiguration,
} from '@coveo/headless-react/ssr';
export const engineDefinition = defineSearchEngine({
configuration: {
...getSampleSearchEngineConfiguration(),
analytics: { enabled: false },
},
controllers: {
searchBox: defineSearchBox(),
resultList: defineResultList(),
authorFacet: defineFacet({ options: { field: "author" } }),
sourceFacet: defineFacet({ options: { field: "source" } }),
},
});
Fetch the static state on the server side using your engine definition, as in the following example:
const staticState = await engineDefinition.fetchStaticState();
// ... Render your UI using the `staticState`.
Fetch the hydrated state on the client side using your engine definition and the static state, as in the following example:
'use client';
// ...
const hydratedState = await engineDefinition.hydrateStaticState({
searchAction: staticState.searchAction,
});
// ... Update your UI using the `hydratedState`.
Once you have the hydrated state, you can add interactivity to the page.
Engine definitions contain hooks and context providers to help build your UI components.
Engine definitions contain different kinds of hooks for React and Next.js.
useEngine hook:
The following is an example of how you would build facet components for the same engine definition used in the previous examples.
If you’re using Next.js with the App Router, any file which uses these hooks must begin with the ’use client'` directive.
'use client';
import { engineDefinition } from '...';
const { useAuthorFacet, useSourceFacet } = engineDefinition.controllers; ①
export function AuthorFacet() {
const { state, methods } = useAuthorFacet();
return <BaseFacet state={state} methods={methods} />;
}
export function SourceFacet() {
const { state, methods } = useSourceFacet();
return <BaseFacet state={state} methods={methods} />;
}
function BaseFacet({
state,
methods,
}: ReturnType<typeof useAuthorFacet | typeof useSourceFacet>) {
// ... Rendering logic
}
useAuthorFacet and useSourceFacet hooks are automatically generated by Headless.
They’re named after the authorFacet and sourceFacet controller map entries, but are automatically capitalized and prefixed with "use" by Headless.To use hooks in your UI components, these UI components must be wrapped with one of the context providers contained in their corresponding engine definition.
StaticStateProvider:
undefined.useEngine hook with undefined.HydratedStateProvider:
useEngine hook with an engine.Using these new providers, we have the necessary components to complete the full loop and implement SSR.
The following example demonstrates how to replace the StaticStateProvider with the HydratedStateProvider once you have the hydrated state, by making a custom component that takes on the responsibility of hydration and choosing the provider.
If you’re using Next.js with the App Router, any file which uses these hooks must begin with the ’use client'` directive.
'use client';
import { useEffect, useState, PropsWithChildren } from 'react';
import { engineDefinition } from '...';
import {
InferStaticState,
InferHydratedState,
} from '@coveo/headless-react/ssr';
const { hydrateStaticState, StaticStateProvider, HydratedStateProvider } =
engineDefinition; ①
type StaticState = InferStaticState<typeof engineDefinition>;
type HydratedState = InferHydratedState<typeof engineDefinition>; ②
export function EngineStateProvider({
staticState,
children,
}: PropsWithChildren<{ staticState: StaticState }>) {
const [hydratedState, setHydratedState] = useState<HydratedState | null>( ③
null
);
useEffect(() => { ④
hydrateStaticState({
searchAction: staticState.searchAction,
}).then(setHydratedState);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (!hydratedState) { ⑤
return (
<StaticStateProvider controllers={staticState.controllers}>
{children}
</StaticStateProvider>
);
}
return ( ⑥
<HydratedStateProvider
engine={hydratedState.engine}
controllers={hydratedState.controllers}
>
{children}
</HydratedStateProvider>
);
}
StaticState and HydratedState to improve readability.useState to allow switching between no hydrated state or rendering on the server and hydration completed.
Here, null means that either hydration isn’t complete or it’s currently rendering on the server.
In both cases, you’ll want to render the static state.useEffect to only hydrate on the client side.StaticStateProvider until hydration has completed.StaticStateProvider with the HydratedStateProvider.Here’s an example of how you would use this component in a Next.js App Router page:
import { engineDefinition } from '...';
import { SearchPageProvider } from '...';
import { ResultList } from '...';
import { SearchBox } from '...';
import { AuthorFacet, SourceFacet } from '...';
const { fetchStaticState } = engineDefinition; ①
export default async function Search() { ②
const staticState = await fetchStaticState({
controllers: {/*...*/},
});
return (
<SearchPageProvider staticState={staticState}>
<SearchBox />
<ResultList />
<AuthorFacet />
<SourceFacet />
</SearchPageProvider>
);
}
For more advanced use cases, such as dispatching actions or interacting with the engine on the server side, refer to the following articles: