Skip to content

Instantly share code, notes, and snippets.

@digitalParkour
Last active July 9, 2024 22:27
Show Gist options
  • Save digitalParkour/5b97b6176ae1aaebc1a44d6b999dbe88 to your computer and use it in GitHub Desktop.
Save digitalParkour/5b97b6176ae1aaebc1a44d6b999dbe88 to your computer and use it in GitHub Desktop.
Sitecore Plugin - React Query Hydration
// 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>
);
};
// 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;
// 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;
// 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);
};
// 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 dehydrationPropPlugin = new DehydratedStatePlugin();
// 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;
}
// EDIT FILE: /src/lib/page-props.ts
// ...
import { DehydratedState } from '@tanstack/react-query';
export type SitecorePageProps = {
// ...
dehydratedState?: DehydratedState;
// ^ ADD page prop
};
// 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;
// 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;
// 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;
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>
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment