Hooks for managing, caching and syncing asynchronous and remote data in React
—
Server-side rendering support with state dehydration and hydration for seamless SSR/SSG integration. React Query provides utilities to serialize server-side data and rehydrate it on the client.
Hook for hydrating QueryClient with server-side rendered state.
/**
* Hydrate QueryClient with server-side state
* @param state - Dehydrated state from server
* @param options - Hydration configuration options
*/
function useHydrate(
state: unknown,
options?: HydrateOptions & ContextOptions
): void;
interface HydrateOptions {
/** Function to determine which queries should be hydrated */
shouldDehydrateQuery?: ShouldDehydrateQueryFunction;
/** Function to determine which mutations should be hydrated */
shouldDehydrateMutation?: ShouldDehydrateMutationFunction;
/** Default options for hydrated queries */
defaultOptions?: {
queries?: QueryOptions;
mutations?: MutationOptions;
};
}
interface ContextOptions {
/** Custom React context to use */
context?: React.Context<QueryClient | undefined>;
}
type ShouldDehydrateQueryFunction = (query: Query) => boolean;
type ShouldDehydrateMutationFunction = (mutation: Mutation) => boolean;Usage Examples:
import { useHydrate } from "react-query";
// Basic hydration
function App({ dehydratedState }: { dehydratedState: any }) {
useHydrate(dehydratedState);
return (
<div>
<UserProfile />
<PostsList />
</div>
);
}
// Hydration with options
function AppWithOptions({ dehydratedState }: { dehydratedState: any }) {
useHydrate(dehydratedState, {
defaultOptions: {
queries: {
staleTime: 60 * 1000 // 1 minute
}
}
});
return <MainContent />;
}
// Conditional hydration
function ConditionalHydration({ dehydratedState, shouldHydrate }: {
dehydratedState: any;
shouldHydrate: boolean;
}) {
useHydrate(shouldHydrate ? dehydratedState : undefined);
return <Content />;
}Component wrapper for hydrating server-side state.
/**
* Component that hydrates QueryClient with server-side state
* @param props - Hydration props with state and options
* @returns Children with hydrated state
*/
function Hydrate(props: HydrateProps): React.ReactElement;
interface HydrateProps {
/** Dehydrated state from server */
state?: unknown;
/** Hydration configuration options */
options?: HydrateOptions;
/** Child components to render */
children?: React.ReactNode;
}Usage Examples:
import { Hydrate, QueryClient, QueryClientProvider } from "react-query";
// Basic component usage
function App({ pageProps }: { pageProps: any }) {
const queryClient = new QueryClient();
return (
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<MyApp {...pageProps} />
</Hydrate>
</QueryClientProvider>
);
}
// Next.js integration
function MyApp({ Component, pageProps }: AppProps) {
const [queryClient] = useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<Component {...pageProps} />
</Hydrate>
</QueryClientProvider>
);
}
// Multiple hydration levels
function NestedHydration({
globalState,
pageState,
children
}: {
globalState: any;
pageState: any;
children: React.ReactNode;
}) {
return (
<Hydrate state={globalState}>
<div>
<GlobalNav />
<Hydrate state={pageState}>
{children}
</Hydrate>
</div>
</Hydrate>
);
}Context provider for tracking restoration state during SSR hydration.
/**
* Provider for restoration state context
* Used internally to track hydration state
*/
const IsRestoringProvider: React.Provider<boolean>;
/**
* Hook to check if app is currently restoring from SSR
* @returns Boolean indicating if restoration is in progress
*/
function useIsRestoring(): boolean;Usage Examples:
import { useIsRestoring, IsRestoringProvider } from "react-query";
// Check restoration state
function MyComponent() {
const isRestoring = useIsRestoring();
if (isRestoring) {
return <div>Restoring from server...</div>;
}
return <div>Client-side rendering active</div>;
}
// Custom restoration provider (rarely needed)
function CustomRestorationProvider({ children }: { children: React.ReactNode }) {
const [isRestoring, setIsRestoring] = useState(true);
useEffect(() => {
// Custom logic to determine when restoration is complete
const timer = setTimeout(() => setIsRestoring(false), 100);
return () => clearTimeout(timer);
}, []);
return (
<IsRestoringProvider value={isRestoring}>
{children}
</IsRestoringProvider>
);
}Low-level functions for dehydrating and hydrating QueryClient state.
/**
* Serialize QueryClient state for server-side rendering
* @param client - QueryClient instance to dehydrate
* @param options - Dehydration configuration
* @returns Serializable state object
*/
function dehydrate(
client: QueryClient,
options?: DehydrateOptions
): DehydratedState;
/**
* Restore QueryClient state from serialized data
* @param client - QueryClient instance to hydrate
* @param dehydratedState - Serialized state from dehydrate
* @param options - Hydration configuration
*/
function hydrate(
client: QueryClient,
dehydratedState: unknown,
options?: HydrateOptions
): void;
interface DehydrateOptions {
/** Function to determine which queries should be dehydrated */
shouldDehydrateQuery?: ShouldDehydrateQueryFunction;
/** Function to determine which mutations should be dehydrated */
shouldDehydrateMutation?: ShouldDehydrateMutationFunction;
}
interface DehydratedState {
/** Serialized queries */
queries: Array<{
queryKey: QueryKey;
queryHash: string;
state: QueryState;
}>;
/** Serialized mutations */
mutations: Array<{
mutationKey?: MutationKey;
state: MutationState;
}>;
}Complete Next.js SSR/SSG setup with React Query:
// pages/_app.tsx
import { useState } from 'react';
import { Hydrate, QueryClient, QueryClientProvider } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';
import type { AppProps } from 'next/app';
export default function MyApp({ Component, pageProps }: AppProps) {
const [queryClient] = useState(() => new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
refetchOnWindowFocus: false,
},
},
}));
return (
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<Component {...pageProps} />
</Hydrate>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
// pages/users/[id].tsx
import { GetServerSideProps } from 'next';
import { QueryClient, dehydrate } from 'react-query';
export default function UserPage({ userId }: { userId: string }) {
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
});
const { data: posts } = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetchUserPosts(userId)
});
return (
<div>
<h1>{user?.name}</h1>
<PostsList posts={posts} />
</div>
);
}
export const getServerSideProps: GetServerSideProps = async ({ params }) => {
const queryClient = new QueryClient();
const userId = params?.id as string;
// Prefetch data on server
await queryClient.prefetchQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
});
await queryClient.prefetchQuery({
queryKey: ['posts', userId],
queryFn: () => fetchUserPosts(userId)
});
return {
props: {
dehydratedState: dehydrate(queryClient),
userId
}
};
};Only hydrating specific queries to reduce bundle size:
// Server-side dehydration with filtering
export const getServerSideProps: GetServerSideProps = async () => {
const queryClient = new QueryClient();
// Prefetch multiple queries
await Promise.all([
queryClient.prefetchQuery({
queryKey: ['user', 'current'],
queryFn: fetchCurrentUser
}),
queryClient.prefetchQuery({
queryKey: ['posts', 'popular'],
queryFn: fetchPopularPosts
}),
queryClient.prefetchQuery({
queryKey: ['analytics', 'daily'],
queryFn: fetchDailyAnalytics
})
]);
return {
props: {
dehydratedState: dehydrate(queryClient, {
shouldDehydrateQuery: (query) => {
// Only dehydrate user and posts, not analytics
const queryKey = query.queryKey[0] as string;
return ['user', 'posts'].includes(queryKey);
}
})
}
};
};
// Client-side selective hydration
function App({ dehydratedState }: { dehydratedState: any }) {
useHydrate(dehydratedState, {
shouldDehydrateQuery: (query) => {
// Only hydrate queries that are not stale
return query.state.dataUpdatedAt > Date.now() - 60000; // 1 minute
}
});
return <MainContent />;
}Hydrating data progressively as components mount:
function ProgressiveHydrationApp({ dehydratedState }: { dehydratedState: any }) {
const [hydratedSections, setHydratedSections] = useState<string[]>([]);
const hydrateSection = (section: string) => {
if (!hydratedSections.includes(section)) {
setHydratedSections(prev => [...prev, section]);
}
};
return (
<div>
{/* Always hydrate critical data */}
<Hydrate state={dehydratedState.critical}>
<Header />
</Hydrate>
{/* Progressively hydrate sections */}
<InView onChange={(inView) => inView && hydrateSection('main')}>
{hydratedSections.includes('main') ? (
<Hydrate state={dehydratedState.main}>
<MainContent />
</Hydrate>
) : (
<div>Loading main content...</div>
)}
</InView>
<InView onChange={(inView) => inView && hydrateSection('sidebar')}>
{hydratedSections.includes('sidebar') ? (
<Hydrate state={dehydratedState.sidebar}>
<Sidebar />
</Hydrate>
) : (
<div>Loading sidebar...</div>
)}
</InView>
</div>
);
}Handling errors during server-side prefetching:
// Robust server-side prefetching
export const getServerSideProps: GetServerSideProps = async ({ params }) => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false, // Don't retry on server
staleTime: Infinity, // Keep data fresh until client hydration
},
},
});
const userId = params?.id as string;
try {
// Prefetch critical data
await queryClient.prefetchQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
});
// Prefetch optional data (don't fail if this errors)
await queryClient.prefetchQuery({
queryKey: ['posts', userId],
queryFn: () => fetchUserPosts(userId)
}).catch(error => {
console.warn('Failed to prefetch posts:', error);
});
} catch (error) {
console.error('Failed to prefetch user:', error);
// Return error page or redirect
return {
notFound: true,
};
}
return {
props: {
dehydratedState: dehydrate(queryClient),
userId
}
};
};
// Client-side error handling during hydration
function ErrorBoundaryWithHydration({
children,
dehydratedState
}: {
children: React.ReactNode;
dehydratedState: any;
}) {
const [hydrationError, setHydrationError] = useState<Error | null>(null);
useEffect(() => {
try {
// Attempt hydration
useHydrate(dehydratedState);
} catch (error) {
setHydrationError(error as Error);
}
}, [dehydratedState]);
if (hydrationError) {
return (
<div>
<h2>Hydration Error</h2>
<p>Failed to restore server state. Falling back to client-side fetching.</p>
<button onClick={() => setHydrationError(null)}>
Try Again
</button>
</div>
);
}
return <>{children}</>;
}Provider and hook for tracking SSR hydration restoration state during client-side hydration.
/**
* Provider component for managing restoration state during SSR hydration
* @param value - Boolean indicating if restoration is in progress
* @param children - Child components
*/
function IsRestoringProvider(props: {
value: boolean;
children: React.ReactNode;
}): React.ReactElement;
/**
* Hook to check if queries are currently being restored during SSR hydration
* @returns Boolean indicating if restoration is in progress
*/
function useIsRestoring(): boolean;Usage Examples:
import { IsRestoringProvider, useIsRestoring } from "react-query";
// App-level restoration tracking
function App({ children }: { children: React.ReactNode }) {
const [isRestoring, setIsRestoring] = useState(true);
useEffect(() => {
// Set restoration complete after hydration
const timer = setTimeout(() => setIsRestoring(false), 100);
return () => clearTimeout(timer);
}, []);
return (
<IsRestoringProvider value={isRestoring}>
<div className={isRestoring ? 'restoring' : 'restored'}>
{children}
</div>
</IsRestoringProvider>
);
}
// Component that adapts behavior during restoration
function DataComponent() {
const isRestoring = useIsRestoring();
const { data, isLoading } = useQuery({
queryKey: ['data'],
queryFn: fetchData
});
// Don't show loading state during restoration to prevent flash
if (isRestoring) {
return <div className="skeleton">Loading...</div>;
}
if (isLoading) {
return <div className="spinner">Fetching data...</div>;
}
return <div>{JSON.stringify(data)}</div>;
}
// Conditional rendering based on restoration state
function OptimizedComponent() {
const isRestoring = useIsRestoring();
return (
<div>
{isRestoring ? (
// Show static content during restoration
<div className="static-content">
<h1>Welcome</h1>
<p>Loading your personalized content...</p>
</div>
) : (
// Show dynamic content after restoration
<DynamicContent />
)}
</div>
);
}
// SSR-aware loading states
function SmartLoadingComponent() {
const isRestoring = useIsRestoring();
const { data, isLoading, isFetching } = useQuery({
queryKey: ['smart-data'],
queryFn: fetchSmartData
});
// During restoration, rely on server-rendered content
if (isRestoring) {
return data ? (
<div className="hydrated-content">{data.content}</div>
) : (
<div className="placeholder-content">Content loading...</div>
);
}
// After restoration, show normal loading states
if (isLoading) {
return <div className="loading-spinner">Loading...</div>;
}
if (isFetching) {
return (
<div className="refreshing">
<div className="content">{data?.content}</div>
<div className="refresh-indicator">Updating...</div>
</div>
);
}
return <div className="content">{data?.content}</div>;
}Implementing custom hydration strategies:
function useCustomHydration(dehydratedState: any) {
const queryClient = useQueryClient();
const [isHydrated, setIsHydrated] = useState(false);
useEffect(() => {
if (!dehydratedState || isHydrated) return;
// Custom hydration logic
const hydrateData = async () => {
try {
// Validate dehydrated state
if (!isValidDehydratedState(dehydratedState)) {
console.warn('Invalid dehydrated state, skipping hydration');
return;
}
// Transform data before hydration if needed
const transformedState = transformDehydratedState(dehydratedState);
// Hydrate with custom options
hydrate(queryClient, transformedState, {
defaultOptions: {
queries: {
staleTime: 30 * 1000, // 30 seconds
cacheTime: 5 * 60 * 1000, // 5 minutes
}
}
});
setIsHydrated(true);
} catch (error) {
console.error('Custom hydration failed:', error);
}
};
hydrateData();
}, [dehydratedState, queryClient, isHydrated]);
return isHydrated;
}
function isValidDehydratedState(state: any): boolean {
return (
state &&
typeof state === 'object' &&
Array.isArray(state.queries) &&
Array.isArray(state.mutations)
);
}
function transformDehydratedState(state: any): any {
// Transform or filter the state as needed
return {
...state,
queries: state.queries.filter((query: any) => {
// Only hydrate recent queries
return query.state.dataUpdatedAt > Date.now() - 60000;
})
};
}Install with Tessl CLI
npx tessl i tessl/npm-react-query