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.

What is Dynamic Hydration?
Dynamic Hydration is the process of syncing server-side and client-side data in real-time to provide a smooth user experience. With Sitecore, it uses tools like React Query to efficiently manage and hydrate data across components. This method improves performance by delivering pre-fetched data to the client while allowing for flexible, dynamic updates.
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?
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 ensures efficient data handling and optimal performance through a systematic approach for dynamic hydration with TanStack Query. Setting up the foundation with TanStack Query involves 7 essential steps to streamline global state management in your application. From implementing the React Query hydration boundary to ensuring seamless data hydration, these steps lay the groundwork for efficient server-side data handling and client-side synchronization.
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 – Setting Up QueryClientProvider for Global State Management in Sitecore
This step involves setting up the QueryClientProvider, a crucial component for global state management in Sitecore. This ensures seamless data fetching and state synchronization across your application, laying the foundation for efficient and scalable state management.
- 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; |
Step 2 – Implementing UseQuery to Fetch and Display Data in React Components
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 – Creating a Fetch Function for Data Retrieval
This step emphasizes implementing the fetch function, which acts as the core for retrieving data in your Sitecore application. This function guarantees efficient communication with APIs, ensuring dynamic and reliable data handling throughout your project.
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 – Extending Page Props to Include Server-Side Query Data
In this step, you'll extend the page props by adding a new property, "dehydratedState", to pass server-side query data to your page components. This ensures that your components have immediate access to pre-fetched data, improving performance and user experience.
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 | |
| }; |
Step 5 – Implementing Hydration Boundary for Server-Side Data Injection
This step concentrates on implementing the Hydration Boundary in React Query to seamlessly inject server-side data into your application. By establishing a hydration boundary, you ensure efficient data synchronization between server and client, enhancing the performance of your React Query setup.
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 – Prefetching Query Data for SSG, ISR, and 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 – Aggregating Component-Level Fetch Methods with a 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.
Sitecore Component Examples
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); | |
| }; |
Here is the updated example SitecoreComponent. This Sitecore component example includes an updated SitecoreComponent that demonstrates how to effectively use the dehydrateQueriesAsync utility for seamless data hydration.
| // 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.

