Implement server-side rendering

This is for:

Developer

This article explains how to implement server-side rendering (SSR) using the utilities that are included in the @coveo/headless-react package.

We recommend that you use the Coveo Headless SSR utilities with the latest Next.js App Router, which provides a better experience than the Pages Router. Both are supported, but the Pages Router may result in unexpected behavior. This article uses the App Router paradigm.

Note

You can also consult a working demo of a Headless SSR search page.

Overview

The strategy for implementing SSR in Headless is as follows:

  • In a shared file: Create and export an engine definition.

  • On the server:

    1. Fetch the static state.

    2. Wrap your components or page with a StaticStateProvider and render it.

    3. Send the page and static state to the client.

  • On the client:

    1. Fetch the hydrated state.

    2. Replace the StaticStateProvider with a HydratedStateProvider and render it.

Create an engine definition

  1. 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" } }),
      },
    });
  2. 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`.
  3. 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.

Build the UI components

Engine definitions contain hooks and context providers to help build your UI components.

Use hooks in your UI components

Engine definitions contain different kinds of hooks for React and Next.js.

  1. Controller hooks:

    • For each controller the definition was configured with, a corresponding hook exists which returns

      • The state of its corresponding controller.

      • The methods of its corresponding controller.

    • Each controller hook will automatically re-render the component in which it was called whenever the state of its controller is updated.

  2. The useEngine hook:

    • Returns an engine.

The following is an example of how you would build facet components for the same engine definition used in the previous examples.

Important

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; 1

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
}
1 Extract the utilities that you need from the engine definition. The 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.

Provide the static or hydrated state to the hooks

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.

  1. The StaticStateProvider:

    • Takes a static state as a prop.

    • Provides controller hooks' states with controller states.

    • Provides controller hooks' methods with undefined.

    • Provides the useEngine hook with undefined.

  2. The HydratedStateProvider:

    • Takes a hydrated state as a prop.

    • Provides controller hooks' states with controller states.

    • Provides controller hooks' methods with controller methods.

    • Provides the 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.

Important

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; 1

type StaticState = InferStaticState<typeof engineDefinition>;
type HydratedState = InferHydratedState<typeof engineDefinition>; 2

export function EngineStateProvider({
  staticState,
  children,
}: PropsWithChildren<{ staticState: StaticState }>) {
  const [hydratedState, setHydratedState] = useState<HydratedState | null>( 3
    null
  );

  useEffect(() => { 4
    hydrateStaticState({
      searchAction: staticState.searchAction,
    }).then(setHydratedState);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  if (!hydratedState) { 5
    return (
      <StaticStateProvider controllers={staticState.controllers}>
        {children}
      </StaticStateProvider>
    );
  }

  return ( 6
    <HydratedStateProvider
      engine={hydratedState.engine}
      controllers={hydratedState.controllers}
    >
      {children}
    </HydratedStateProvider>
  );
}
1 Extract the utilities that you need from the engine definition.
2 Declare the StaticState and HydratedState to improve readability.
3 Use 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.
4 Use useEffect to only hydrate on the client side.
5 Render the StaticStateProvider until hydration has completed.
6 When hydration is finished, replace the 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; 1

export default async function Search() { 2
  const staticState = await fetchStaticState({
    controllers: {/*...*/},
  });

  return (
    <SearchPageProvider staticState={staticState}>
      <SearchBox />
      <ResultList />
      <AuthorFacet />
      <SourceFacet />
    </SearchPageProvider>
  );
}
1 Extract the utilities that you need from the engine definition.
2 Anything inside this function will only be executed on the server. See Data Fetching for more information.

What’s next?

For more advanced use cases, such as dispatching actions or interacting with the engine on the server side, refer to the following articles: