The framework agnostic core that powers TanStack Query for data fetching and caching
—
Server-side rendering support with serialization and deserialization of client state for seamless hydration across client-server boundaries and persistent cache storage.
Serialize QueryClient state for transfer or storage.
/**
* Serialize QueryClient state to a transferable format
* Converts queries and mutations to JSON-serializable format
* @param client - QueryClient instance to dehydrate
* @param options - Options controlling what gets dehydrated
* @returns Serializable state object
*/
function dehydrate(client: QueryClient, options?: DehydrateOptions): DehydratedState;
interface DehydrateOptions {
/**
* Function to determine which queries should be dehydrated
* @param query - Query to evaluate for dehydration
* @returns true if query should be included in dehydrated state
*/
shouldDehydrateQuery?: (query: Query) => boolean;
/**
* Function to determine which mutations should be dehydrated
* @param mutation - Mutation to evaluate for dehydration
* @returns true if mutation should be included in dehydrated state
*/
shouldDehydrateMutation?: (mutation: Mutation) => boolean;
/**
* Function to serialize data values
* @param data - Data to serialize
* @returns Serialized data
*/
serializeData?: (data: unknown) => unknown;
}
interface DehydratedState {
/** Serialized mutations */
mutations: Array<DehydratedMutation>;
/** Serialized queries */
queries: Array<DehydratedQuery>;
}
interface DehydratedQuery {
queryHash: string;
queryKey: QueryKey;
state: QueryState;
}
interface DehydratedMutation {
mutationKey?: MutationKey;
state: MutationState;
}Usage Examples:
import { QueryClient, dehydrate } from "@tanstack/query-core";
const queryClient = new QueryClient();
// Basic dehydration
const dehydratedState = dehydrate(queryClient);
// Dehydration with custom filters
const selectiveDehydratedState = dehydrate(queryClient, {
shouldDehydrateQuery: (query) => {
// Only dehydrate successful queries that are not stale
return query.state.status === 'success' && !query.isStale();
},
shouldDehydrateMutation: (mutation) => {
// Only dehydrate pending mutations
return mutation.state.status === 'pending';
},
});
// Dehydration with data transformation
const transformedDehydratedState = dehydrate(queryClient, {
shouldDehydrateQuery: (query) => {
// Skip large data sets in SSR
const dataSize = JSON.stringify(query.state.data).length;
return dataSize < 10000; // Less than 10KB
},
serializeData: (data) => {
// Custom serialization (e.g., handle Dates, BigInts)
return JSON.parse(JSON.stringify(data, (key, value) => {
if (value instanceof Date) {
return { __type: 'Date', value: value.toISOString() };
}
if (typeof value === 'bigint') {
return { __type: 'BigInt', value: value.toString() };
}
return value;
}));
},
});
// Convert to JSON for transfer
const serialized = JSON.stringify(dehydratedState);
// Store in localStorage
localStorage.setItem('react-query-cache', serialized);
// Send to client in SSR
const html = `
<script>
window.__REACT_QUERY_STATE__ = ${JSON.stringify(dehydratedState)};
</script>
`;Restore QueryClient state from dehydrated data.
/**
* Restore QueryClient state from dehydrated data
* Recreates queries and mutations from serialized state
* @param client - QueryClient instance to hydrate
* @param dehydratedState - Previously dehydrated state
* @param options - Options controlling hydration behavior
*/
function hydrate(
client: QueryClient,
dehydratedState: unknown,
options?: HydrateOptions
): void;
interface HydrateOptions {
/**
* Default options to apply to hydrated queries
*/
defaultOptions?: {
queries?: Partial<QueryObserverOptions>;
mutations?: Partial<MutationObserverOptions>;
};
/**
* Function to deserialize data values
* @param data - Data to deserialize
* @returns Deserialized data
*/
deserializeData?: (data: unknown) => unknown;
}Usage Examples:
import { QueryClient, hydrate } from "@tanstack/query-core";
// Client-side hydration
const queryClient = new QueryClient();
// Basic hydration from window object (SSR)
if (typeof window !== 'undefined' && window.__REACT_QUERY_STATE__) {
hydrate(queryClient, window.__REACT_QUERY_STATE__);
}
// Hydration from localStorage
const storedState = localStorage.getItem('react-query-cache');
if (storedState) {
try {
const dehydratedState = JSON.parse(storedState);
hydrate(queryClient, dehydratedState);
} catch (error) {
console.error('Failed to hydrate from localStorage:', error);
localStorage.removeItem('react-query-cache');
}
}
// Hydration with custom deserialization
hydrate(queryClient, dehydratedState, {
deserializeData: (data) => {
// Custom deserialization to handle special types
return JSON.parse(JSON.stringify(data), (key, value) => {
if (value && typeof value === 'object') {
if (value.__type === 'Date') {
return new Date(value.value);
}
if (value.__type === 'BigInt') {
return BigInt(value.value);
}
}
return value;
});
},
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
},
},
});Built-in functions for common dehydration scenarios.
/**
* Default function to determine if a query should be dehydrated
* Only dehydrates successful queries that are not infinite queries
* @param query - Query to evaluate
* @returns true if query should be dehydrated
*/
function defaultShouldDehydrateQuery(query: Query): boolean;
/**
* Default function to determine if a mutation should be dehydrated
* Only dehydrates pending mutations
* @param mutation - Mutation to evaluate
* @returns true if mutation should be dehydrated
*/
function defaultShouldDehydrateMutation(mutation: Mutation): boolean;Usage Examples:
import {
dehydrate,
defaultShouldDehydrateQuery,
defaultShouldDehydrateMutation
} from "@tanstack/query-core";
// Using default functions with custom logic
const dehydratedState = dehydrate(queryClient, {
shouldDehydrateQuery: (query) => {
// Use default logic but exclude certain query keys
if (!defaultShouldDehydrateQuery(query)) {
return false;
}
// Don't dehydrate sensitive data
const sensitiveKeys = ['user-secrets', 'api-keys'];
return !sensitiveKeys.some(key =>
JSON.stringify(query.queryKey).includes(key)
);
},
shouldDehydrateMutation: defaultShouldDehydrateMutation,
});Common patterns for server-side rendering integration.
// Server-side (Next.js getServerSideProps example)
export async function getServerSideProps() {
const queryClient = new QueryClient();
// Prefetch data on server
await queryClient.prefetchQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: () => fetchPosts(),
});
// Dehydrate state
const dehydratedState = dehydrate(queryClient, {
shouldDehydrateQuery: (query) => {
// Only send successful queries to client
return query.state.status === 'success';
},
});
return {
props: {
dehydratedState,
},
};
}
// Client-side component
function MyApp({ dehydratedState }) {
const [queryClient] = useState(() => new QueryClient());
// Hydrate on client mount
useEffect(() => {
if (dehydratedState) {
hydrate(queryClient, dehydratedState);
}
}, [queryClient, dehydratedState]);
return (
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
);
}Implementing persistent caching with automatic dehydration/hydration.
class PersistentQueryClient {
private queryClient: QueryClient;
private persistKey: string;
constructor(persistKey = 'react-query-cache') {
this.persistKey = persistKey;
this.queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours
},
},
});
this.loadFromStorage();
this.setupAutoPersist();
}
private loadFromStorage() {
try {
const stored = localStorage.getItem(this.persistKey);
if (stored) {
const dehydratedState = JSON.parse(stored);
hydrate(this.queryClient, dehydratedState);
}
} catch (error) {
console.error('Failed to load persisted cache:', error);
localStorage.removeItem(this.persistKey);
}
}
private setupAutoPersist() {
// Persist cache periodically
setInterval(() => {
this.persistToStorage();
}, 30000); // Every 30 seconds
// Persist on page unload
window.addEventListener('beforeunload', () => {
this.persistToStorage();
});
}
private persistToStorage() {
try {
const dehydratedState = dehydrate(this.queryClient, {
shouldDehydrateQuery: (query) => {
// Only persist successful, non-stale queries
return query.state.status === 'success' &&
!query.isStale() &&
query.state.dataUpdatedAt > Date.now() - (1000 * 60 * 60); // Less than 1 hour old
},
});
localStorage.setItem(this.persistKey, JSON.stringify(dehydratedState));
} catch (error) {
console.error('Failed to persist cache:', error);
}
}
getClient() {
return this.queryClient;
}
clearPersisted() {
localStorage.removeItem(this.persistKey);
}
}
// Usage
const persistentClient = new PersistentQueryClient();
const queryClient = persistentClient.getClient();Handling cache version changes and data migration.
interface VersionedDehydratedState extends DehydratedState {
version: string;
timestamp: number;
}
class VersionedCache {
private static CURRENT_VERSION = '1.2.0';
private static MAX_AGE = 1000 * 60 * 60 * 24 * 7; // 7 days
static dehydrate(client: QueryClient): VersionedDehydratedState {
const baseState = dehydrate(client);
return {
...baseState,
version: this.CURRENT_VERSION,
timestamp: Date.now(),
};
}
static hydrate(client: QueryClient, state: unknown): boolean {
if (!this.isValidState(state)) {
console.warn('Invalid or outdated cache state, skipping hydration');
return false;
}
const versionedState = state as VersionedDehydratedState;
// Migrate if needed
const migratedState = this.migrate(versionedState);
hydrate(client, migratedState);
return true;
}
private static isValidState(state: unknown): state is VersionedDehydratedState {
if (!state || typeof state !== 'object') return false;
const versionedState = state as VersionedDehydratedState;
// Check version compatibility
if (!versionedState.version || !this.isCompatibleVersion(versionedState.version)) {
return false;
}
// Check age
if (!versionedState.timestamp || Date.now() - versionedState.timestamp > this.MAX_AGE) {
return false;
}
return true;
}
private static isCompatibleVersion(version: string): boolean {
// Simple major version check
const [currentMajor] = this.CURRENT_VERSION.split('.');
const [stateMajor] = version.split('.');
return currentMajor === stateMajor;
}
private static migrate(state: VersionedDehydratedState): DehydratedState {
// Implement migration logic based on version
if (state.version === '1.1.0') {
// Example migration from 1.1.0 to 1.2.0
return {
...state,
queries: state.queries.map(query => ({
...query,
// Add new fields or transform existing ones
})),
};
}
return state;
}
}
// Usage
const dehydratedState = VersionedCache.dehydrate(queryClient);
localStorage.setItem('cache', JSON.stringify(dehydratedState));
// Later...
const storedState = localStorage.getItem('cache');
if (storedState) {
const success = VersionedCache.hydrate(queryClient, JSON.parse(storedState));
if (!success) {
// Handle failed hydration (clear cache, show message, etc.)
localStorage.removeItem('cache');
}
}interface QueryState<TData = unknown, TError = Error> {
data: TData | undefined;
dataUpdateCount: number;
dataUpdatedAt: number;
error: TError | null;
errorUpdateCount: number;
errorUpdatedAt: number;
fetchFailureCount: number;
fetchFailureReason: TError | null;
fetchMeta: FetchMeta | null;
isInvalidated: boolean;
status: QueryStatus;
fetchStatus: FetchStatus;
}
interface MutationState<TData = unknown, TError = Error, TVariables = void, TContext = unknown> {
context: TContext | undefined;
data: TData | undefined;
error: TError | null;
failureCount: number;
failureReason: TError | null;
isPaused: boolean;
status: MutationStatus;
variables: TVariables | undefined;
submittedAt: number;
}
type QueryStatus = 'pending' | 'error' | 'success';
type FetchStatus = 'fetching' | 'paused' | 'idle';
type MutationStatus = 'idle' | 'pending' | 'success' | 'error';
interface FetchMeta extends Record<string, unknown> {}Install with Tessl CLI
npx tessl i tessl/npm-tanstack--query-core