How to Achieve Dynamic Hydration with Sitecore

The following content is a technical guide for enhancing Sitecore code bases.

Without care, implementing data fetching can get messy.

Nearing three million weekly downloads, TanStack Query is a popular choice for good reason. It greatly simplifies data handling.

Following is a guide for how to configure TanStack Query with Sitecore's JSS boilerplate to achieve dynamic hydration.

What is dynamic hydration? Well, Sitecore CMS provides dynamic pages with dynamic design orchestrated by authors dropping components onto pages. So, how can a developer best pair server-side data fetching to the component that needs it and how does that affect client-side component code?

Spoiler: it doesn't need to affect the code at all! This is dynamic hydration.

For context, as a developer, we’d like a way to preload data server side during the static site generation (SSG/ISR) to bake it into the client bundle and provide the data to any client side components that need it. Ideally, this can be managed within the scope of the component to fully embrace component-driven design. Sitecore plugins make this clean and simple.

Professional using laptop to update webpage contents using CMS, showcasing business success with Sitecore's dynamic hydration

Intro to Dynamic Hydration with Sitecore

What if I told you this simple useQuery block...

 const ShowCatFact = (): JSX.Element => {
 const query = useQuery({
 queryKey: ['uniqueString'], // unique key to access fetched data
 queryFn: fetchCatFactAsync, // function to (re)fetch data
 });
  
 // ...
 return <p>{query.isLoading ? '...' : query.data.fact}</p>
 }

 

... can be set up in your code to handle:

All the normal React Query benefits:

  • Succinct data fetch management (data, loading, success, error)
  • TypeScript support
  • Client cache of data
    • Stale while revalidate - show old data when fetching fresh data
    • Controls for when to refetch data
  • Sharing data result across components
  • And more!

But also, dynamic hydration:

  • Seamless Static Site Generation (SSG), Server-Side Rendering (SSR), and Incremental Static Regeneration (ISR) support – hydrate client with server-side data
    • This means no client-side fetch, data is used in server-side rendering
    • Seamless – React components don't care. Whether data was fetched server-side or not, the useQuery code doesn't change.
  • Component level data fetch – with Sitecore's component level data fetching, server-side data calls can live at the Sitecore component level
  • And no extra code once set up!

This extra magic leverages Sitecore's plugin pattern, adding a step in the page-props-factory. This solution is designed for the Next.js Pages Router and Sitecore JSS SDK.

Step-By-Step Guide to Server-Side Data Prefetching in Sitecore

The process of setting up server-side data prefetching in Sitecore, ensuring efficient data handling and optimal performance through a systematic approach for dynamic hydration with TanStack Query.

Setting Up the Foundation with TanStack Query

For completeness, let's cover the initial setup. This layer lets us do client-side queries with useQuery.

Step 1 – QueryClientProvider

Add TanStack Query: npm i @tanstack/react-query (Docs)

  • Also add dev tools: npm i @tanstack/react-query-devtools

Initialize QueryClientProvider in app root

  • Implement as separate file, ReactQueryClientProvider.tsx
  • Add ReactQueryClientProvider to _app.tsx
 // NEW FILE: /src/lib/providers/ReactQueryClientProvider.tsx
  
 import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
 import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
 import { useState } from 'react';
  
 type ReacQueryClientProviderProps = {
 children: React.ReactElement;
 };
  
 function ReacQueryClientProvider({ children }: ReacQueryClientProviderProps): JSX.Element {
 // ReactQuery: use state for queryClient, which ensures each request has its own cache:
 const [queryClient] = useState(
 () =>
 new QueryClient({
 defaultOptions: {
 queries: {
 // Any global behavior defaults here:
 refetchOnMount: false,
 refetchOnReconnect: false,
 refetchOnWindowFocus: false,
 // With SSR, we usually want to set some default staleTime
 // above 0 to avoid refetching immediately on the client
 staleTime: Infinity,
 gcTime: Infinity,
 },
 },
 })
 );
  
 return (
 <QueryClientProvider client={queryClient}>
 {/* ^ ADD the QueryClient provider with our shared client (cache) instance */}
 {children}
 <ReactQueryDevtools />
 {/* ^ can ADD dev tools */}
 </QueryClientProvider>
 );
 }
  
 export default ReactQueryClientProvider;

 

// EDIT FILE: /src/pages/_app.tsx
  
 import ReactQueryClientProvider from '@/lib/providers/ReactQueryClientProvider';
 import '@/sass/main.scss';
 import { SitecorePageProps } from '@/types/props/page-props';
 import { I18nProvider } from 'next-localization';
 import type { AppProps } from 'next/app';
 import Bootstrap from 'src/Bootstrap';
  
 function App({ Component, pageProps }: AppProps<SitecorePageProps>): JSX.Element {
 const { dictionary, locale, ...rest } = pageProps;
  
 return (
 <>
 <Bootstrap {...pageProps} />
 <I18nProvider lngDict={dictionary} locale={locale}>
 <ReactQueryClientProvider>
 {/* ^ ADD new QueryClientProvider HERE to cover every page route */}
 <Component {...rest} />
 </ReactQueryClientProvider>
 </I18nProvider>
 </>
 );
 }
  
 export default App;
view raw_app.tsx hosted with ❤ by GitHub

 

Step 2 – Demo UseQuery

Now that we have the global query client provider, we can put useQuery in any React component. For example:

// NEW FILE: /src/components/example/ShowCatFact.tsx
  
 import { useQuery } from '@tanstack/react-query';
 import { CatFactQueryKey, fetchCatFactAsync } from './fetch';
  
 const ShowCatFact = (): JSX.Element => {
 const query = useQuery({
 queryKey: [CatFactQueryKey],
 queryFn: fetchCatFactAsync,
 });
  
 return (
 <div>
 <h1>Cat Fact</h1>
 <p>{query.isLoading ? '...' : query.data.fact}</p>
 </div>
 );
 };
  
 export default ShowCatFact;

 

Step 3 – Implement Fetch Function

Here is the example fetch method referenced above:

 // NEW FILE: /src/components/example/fetch.ts
  
 import { NativeDataFetcher } from '@sitecore-jss/sitecore-jss-nextjs';
  
 /* ==================== HTTP Fetch Example ================================
 /*
 * A Fetch Method implements how to get the data.
 * Here we will return a random cat fact from http endpoint
 */
  
 export const CatFactQueryKey = 'catFactQueryKey';
 export type TExampleCatFactResult = {
 fact: string;
 length: number;
 };
  
 export async function fetchCatFactAsync(): Promise<TExampleCatFactResult> {
 /* Use Sitecore Fetcher which has built-in debug logging */
 const fetcher = new NativeDataFetcher();
 const response = await fetcher.fetch<TExampleCatFactResult>('https://catfact.ninja/fact');
 return response.data;
 }

 

Implementing a Dynamic Hydration Layer for Optimal Performance

So far, we've enabled client-side query management and the ability to share data between components using the same cache key. Next, we'll implement the server-side prefetching with dynamic hydration.

Tip: Getting the data server-side for SSG/ISR can reduce the number of calls to your remote data endpoint which in many cases can reduce API costs.

Step 4 – Extend Page Props

We need to pass our server-side query data to the page components, so let's add a new page prop "dehydratedState":

// EDIT FILE: /src/lib/page-props.ts
  
 // ...
 import { DehydratedState } from '@tanstack/react-query';
  
 export type SitecorePageProps = {
 // ...
 dehydratedState?: DehydratedState;
 // ^ ADD page prop
 };
view rawpage-props.ts hosted with ❤ by GitHub

 

Step 5 – Hydration Boundary

We need to implement TanStack Query's Hydration Boundary to inject our server-side data. Let's do it in our dynamic page route:

 // EDIT FILE: /src/pages/[[...path]].tsx
  
 // ...
 import { HydrationBoundary } from '@tanstack/react-query';
  
 const SitecorePage = ({
 // ...
 dehydratedState,
 // ^ EXPOSE new page prop
 }: SitecorePageProps): JSX.Element => {
  
 // ...
  
 return (
 <HydrationBoundary state={dehydratedState}>
 {/* ^ ADD HydrationBoundary, set state to page prop resolved by page-props-factory plugin
 This can be moved to _app.tsx to apply to all routes.
 Kept here to support all Sitecore routes, allowing other routes to opt-in. */}
 <ComponentPropsContext value={componentProps}>
 {/* ... */}
 </ComponentPropsContext>
 </HydrationBoundary>
 );
 };

 

Step 6 – Query Data (SSG/ISR/SSR)

Let's populate that page prop with an example server-side query. We'll show how to do it at Sitecore's component level:

// NEW FILE: /src/components/SitecoreComponent.tsx
  
 import ShowCatFact from '@/components/example/ShowCatFact';
 import { CatFactQueryKey, fetchCatFactAsync } from '@/components/example/fetch';
 import { GetStaticComponentProps } from '@sitecore-jss/sitecore-jss-nextjs';
 import { QueryClient, dehydrate } from '@tanstack/react-query';
  
 const FetchExample = (): JSX.Element => {
 return <ShowCatFact />;
 };
  
 // This is called by Sitecore when Page route uses GetStaticProps, for SSG/ISR
 export const getStaticProps: GetStaticComponentProps = async () => {
 // 1. Instantiate QueryClient server side
 const queryClient = new QueryClient();
  
 // 2. Run one or more queries to add results to cache
 await queryClient.prefetchQuery({
 queryKey: [CatFactQueryKey],
 queryFn: fetchCatFactAsync,
 });
  
 return {
 props: {
 // 3. send dehydrated result cache for client side component props
 dehydratedState: dehydrate(queryClient),
 },
 };
 };
  
 export default FetchExample;

 

Step 7 – Page Props Plugin

And now the grand finale, how to aggregate all the component level fetch methods into our new dehydratedState page prop:

 // NEW FILE: /src/lib/page-props-factory/plugins/dehydration-props.ts
  
 import { DehydratedStateProps } from '@/types/props/component-props';
 import { SitecorePageProps } from '@/types/props/page-props';
 import { DehydratedState } from '@tanstack/react-query';
 import merge from 'ts-deepmerge';
 import { Plugin } from '..';
  
 /*
 React Query Seamless Hydration Plugin
  
 Allow component level GetServerSideProps|GetStaticProps to fetch and dehydrate data.
 Aggregate here to page props level to send to <HydrationBoundary /> (** see [[...path]].tsx)
 This passes server side data to client side cache, saving redundant client-side queries.
 */
 class DehydratedStatePlugin implements Plugin {
 order = 3; // run after component-props plugin
  
 async exec(props: SitecorePageProps) {
 // Bubble up all component level dehydratedState props to this page level prop
 let data: DehydratedState = {
 mutations: [],
 queries: [],
 };
  
 // iterate all components declared in layout service response
 if (props.componentProps) {
 for (const p in props.componentProps) {
 const thisProps = props.componentProps[p] as DehydratedStateProps;
 // if has query state, merge it up
 if (!!thisProps?.props?.dehydratedState) {
 data = merge(data, thisProps.props?.dehydratedState);
 }
 }
 }
  
 // pass combined state to page props
 props.dehydratedState = data;
  
 return props;
 }
 }
  
 // Export name must be <normalizedFileName>+"Plugin"
 export const dehydrationPropsPlugin = new DehydratedStatePlugin();

 

Important: notice the order property. This step must come after the component-props step.

Also, after adding that plugin file, run Sitecore's bootstrap task npm run bootstrap to kick off Sitecore's script that builds the temp file needed for that plugin.

Validating the Setup: How to Test Dynamic Hydration

A simple test to verify if all is working is to open up the browser dev console. When you don't have the server-side query, you will see the network request for your data fetch. When you have the server-side prefetch and you don't see the network request for the cat fact client side, then you know the hydration is working as expected.

Wrapping Up: Seamless Data Fetching Across Environments

With this plumbing in place, we can implement useQuery in any React component without concern of when the data is loaded. For cases when we want prefetching server-side, we can opt in by passing the dehydratedState page prop from GetStaticProps/GetServerSideProps. We can even do this at the Sitecore component level GetStaticProps/GetServerSideProps to isolate usage for that component.

Bonus!

As an extra, here is a utility to collapse the server-side work into a one liner (see example usage in comments below):

 // NEW FILE: /src/lib/utils/dehydrateQueries.ts
  
 import { DehydratedState, QueryClient, dehydrate } from '@tanstack/react-query';
  
 export type QueryInput = [string[], () => unknown];
  
 /*
 Fetch one or more queries in parallel server-side.
 Returns dehydrated query client for page prop.
  
 Input format - [queryKey, queryFn] pairs:
  
 await dehydrateQueriesAsync([
 [['queryKey'], fetchFunction],
 [['queryKey', var1, var2], () => myFetch(var1, var2)],
 ])
  
 Example usage: (in GetStaticProps/GetServerSideProps):
  
 return {
 props: {
 dehydratedState: await dehydrateQueriesAsync([[CatFactQueryKey], fetchCatFactAsync]),
 },
 };
  
 */
 export const dehydrateQueriesAsync = async (queries: QueryInput[]): Promise<DehydratedState> => {
 const queryClient = new QueryClient();
  
 await Promise.all(
 queries.map((x): void => {
 const [queryKey, queryFn] = x;
 queryClient.prefetchQuery({
 queryKey,
 queryFn,
 });
 })
 );
  
 return dehydrate(queryClient);
 };

 

And here is the updated example SitecoreComponent using that dehydrateQueriesAsync utility:

// REPLACE NEW FILE: /src/components/SitecoreComponent.tsx
import ShowCatFact from '@/components/example/ShowCatFact';
import { CatFactQueryKey, fetchCatFactAsync } from '@/components/example/fetch';
import { GetStaticComponentProps } from '@sitecore-jss/sitecore-jss-nextjs';
import { QueryClient, dehydrate } from '@tanstack/react-query';
const FetchExample = (): JSX.Element => {
return <ShowCatFact />;
};
export const getStaticProps: GetStaticComponentProps = async () => {
return {
props: {
// Condense any server-side queries to one line:
dehydratedState: await dehydrateQueriesAsync([[CatFactQueryKey], fetchCatFactAsync]),
},
};
};
export default FetchExample;

Work with a Certified Sitecore Partner

Americaneagle.com is a Sitecore website development partner with years of experience devoted to providing exceptional end-to-end digital experiences that drive results. We offer a range of Sitecore development services, some of which include enterprise implementations and integrations, ongoing support and managed services, solution audits, version updates, and more.

To learn more about how our Sitecore practice, contact us today.


About Author

James Gregory
James Gregory is a seasoned Sitecore architect and three-time Sitecore Hackathon winner with a rich history at Americaneagle.com, having been with the company since 2013. He's on a mission to simplify and optimize Sitecore experiences with a focus on intuitive design. James aims to make complex and tedious tasks easier and faster, helping businesses maximize their Sitecore investments and streamline processes. Beyond coding, he enjoys disc golf and strategy board games. Join James on his journey to enhance the Sitecore ecosystem and discover innovative ways to elevate your digital experiences.


Featured Posts