CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-tanstack--query-core

The framework agnostic core that powers TanStack Query for data fetching and caching

Pending
Overview
Eval results
Files

hydration.mddocs/

Hydration & Serialization

Server-side rendering support with serialization and deserialization of client state for seamless hydration across client-server boundaries and persistent cache storage.

Capabilities

Dehydration

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>
`;

Hydration

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
    },
  },
});

Default Dehydration Functions

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,
});

SSR Integration Patterns

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>
  );
}

Persistent Cache Implementation

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();

Cache Versioning and Migration

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');
  }
}

Core Types

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

docs

browser-integration.md

cache-management.md

client-management.md

hydration.md

index.md

infinite-queries.md

mutations.md

query-observers.md

query-operations.md

utilities.md

tile.json