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

mutations.mddocs/

Mutations

Mutation management for data modifications with optimistic updates, rollback capabilities, side effect handling, and automatic query invalidation.

Capabilities

MutationObserver

Observer for tracking and executing mutations with automatic state management.

/**
 * Observer for managing mutations and tracking their state
 * Provides reactive updates for mutation progress, success, and error states
 */
class MutationObserver<
  TData = unknown,
  TError = Error,
  TVariables = void,
  TContext = unknown
> {
  constructor(client: QueryClient, options: MutationObserverOptions<TData, TError, TVariables, TContext>);
  
  /**
   * Execute the mutation with the given variables
   * @param variables - Variables to pass to the mutation function
   * @param options - Additional options for this specific mutation execution
   * @returns Promise resolving to the mutation result
   */
  mutate(variables: TVariables, options?: MutateOptions<TData, TError, TVariables, TContext>): Promise<TData>;
  
  /**
   * Get the current result snapshot
   * @returns Current mutation observer result
   */
  getCurrentResult(): MutationObserverResult<TData, TError, TVariables, TContext>;
  
  /**
   * Subscribe to mutation state changes
   * @param onStoreChange - Callback called when mutation state changes
   * @returns Unsubscribe function
   */
  subscribe(onStoreChange: (result: MutationObserverResult<TData, TError, TVariables, TContext>) => void): () => void;
  
  /**
   * Update mutation observer options
   * @param options - New options to merge
   */
  setOptions(options: MutationObserverOptions<TData, TError, TVariables, TContext>): void;
  
  /**
   * Reset the mutation to its initial state
   * Clears data, error, and resets status to idle
   */
  reset(): void;
  
  /**
   * Destroy the observer and cleanup subscriptions
   */
  destroy(): void;
}

interface MutationObserverOptions<
  TData = unknown,
  TError = Error,
  TVariables = void,
  TContext = unknown
> {
  /**
   * Optional mutation key for identification and scoping
   */
  mutationKey?: MutationKey;
  
  /**
   * The mutation function that performs the actual mutation
   * @param variables - Variables passed to the mutation
   * @returns Promise resolving to the mutation result
   */
  mutationFn?: MutationFunction<TData, TVariables>;
  
  /**
   * Function called before the mutation executes
   * Useful for optimistic updates
   * @param variables - Variables being passed to mutation
   * @returns Context value passed to other callbacks
   */
  onMutate?: (variables: TVariables) => Promise<TContext> | TContext;
  
  /**
   * Function called if the mutation succeeds
   * @param data - Data returned by the mutation
   * @param variables - Variables that were passed to mutation
   * @param context - Context returned from onMutate
   */
  onSuccess?: (data: TData, variables: TVariables, context: TContext) => Promise<unknown> | unknown;
  
  /**
   * Function called if the mutation fails
   * @param error - Error thrown by the mutation
   * @param variables - Variables that were passed to mutation
   * @param context - Context returned from onMutate
   */
  onError?: (error: TError, variables: TVariables, context: TContext) => Promise<unknown> | unknown;
  
  /**
   * Function called when the mutation settles (success or error)
   * @param data - Data returned by mutation (undefined if error)
   * @param error - Error thrown by mutation (null if success)
   * @param variables - Variables that were passed to mutation
   * @param context - Context returned from onMutate
   */
  onSettled?: (data: TData | undefined, error: TError | null, variables: TVariables, context: TContext) => Promise<unknown> | unknown;
  
  /**
   * Number of retry attempts for failed mutations
   */
  retry?: RetryValue<TError>;
  
  /**
   * Delay between retry attempts
   */
  retryDelay?: RetryDelayValue<TError>;
  
  /**
   * Whether to throw errors or handle them in the result
   */
  throwOnError?: ThrowOnError<TData, TError, TVariables, TContext>;
  
  /**
   * Additional metadata for the mutation
   */
  meta?: MutationMeta;
  
  /**
   * Network mode for the mutation
   */
  networkMode?: NetworkMode;
  
  /**
   * Global mutation ID for scoped mutations
   * Only one mutation with the same scope can run at a time
   */
  scope?: {
    id: string;
  };
}

interface MutationObserverResult<
  TData = unknown,
  TError = Error,
  TVariables = void,
  TContext = unknown
> {
  /** The data returned by the mutation */
  data: TData | undefined;
  
  /** The error thrown by the mutation */
  error: TError | null;
  
  /** The variables passed to the mutation */
  variables: TVariables | undefined;
  
  /** Whether the mutation is currently idle */
  isIdle: boolean;
  
  /** Whether the mutation is currently pending */
  isPending: boolean;
  
  /** Whether the mutation completed successfully */
  isSuccess: boolean;
  
  /** Whether the mutation failed */
  isError: boolean;
  
  /** Whether the mutation is paused */
  isPaused: boolean;
  
  /** The current status of the mutation */
  status: MutationStatus;
  
  /** Number of times the mutation has failed */
  failureCount: number;
  
  /** The reason for the most recent failure */
  failureReason: TError | null;
  
  /** Function to execute the mutation */
  mutate: (variables: TVariables, options?: MutateOptions<TData, TError, TVariables, TContext>) => void;
  
  /** Function to execute the mutation and return a promise */
  mutateAsync: (variables: TVariables, options?: MutateOptions<TData, TError, TVariables, TContext>) => Promise<TData>;
  
  /** Function to reset the mutation state */
  reset: () => void;
  
  /** Context returned from onMutate */
  context: TContext | undefined;
  
  /** Timestamp when the mutation was submitted */
  submittedAt: number;
}

interface MutateOptions<
  TData = unknown,
  TError = Error,
  TVariables = void,
  TContext = unknown
> {
  onSuccess?: (data: TData, variables: TVariables, context: TContext) => Promise<unknown> | unknown;
  onError?: (error: TError, variables: TVariables, context: TContext) => Promise<unknown> | unknown;
  onSettled?: (data: TData | undefined, error: TError | null, variables: TVariables, context: TContext) => Promise<unknown> | unknown;
  throwOnError?: ThrowOnError<TData, TError, TVariables, TContext>;
}

Usage Examples:

import { QueryClient, MutationObserver } from "@tanstack/query-core";

const queryClient = new QueryClient();

// Create mutation observer
const mutationObserver = new MutationObserver(queryClient, {
  mutationFn: async (userData) => {
    const response = await fetch('/api/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(userData),
    });
    return response.json();
  },
  onSuccess: (data, variables) => {
    console.log('User created:', data);
    
    // Invalidate users list
    queryClient.invalidateQueries({ queryKey: ['users'] });
  },
  onError: (error, variables) => {
    console.error('Failed to create user:', error);
  },
});

// Subscribe to mutation state
const unsubscribe = mutationObserver.subscribe((result) => {
  console.log('Status:', result.status);
  console.log('Is pending:', result.isPending);
  console.log('Data:', result.data);
  console.log('Error:', result.error);
  
  if (result.isSuccess) {
    console.log('Mutation succeeded with data:', result.data);
  }
  
  if (result.isError) {
    console.error('Mutation failed with error:', result.error);
  }
});

// Execute mutation
try {
  const newUser = await mutationObserver.mutate({
    name: 'John Doe',
    email: 'john@example.com',
  });
  console.log('Created user:', newUser);
} catch (error) {
  console.error('Mutation failed:', error);
}

// Reset mutation state
mutationObserver.reset();

// Cleanup
unsubscribe();
mutationObserver.destroy();

Optimistic Updates

Implementing optimistic updates with proper rollback handling.

// Optimistic updates with rollback
const updateUserMutation = new MutationObserver(queryClient, {
  mutationFn: async ({ id, updates }) => {
    const response = await fetch(`/api/users/${id}`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(updates),
    });
    if (!response.ok) throw new Error('Update failed');
    return response.json();
  },
  
  // Optimistic update
  onMutate: async ({ id, updates }) => {
    // Cancel outgoing refetches so they don't overwrite optimistic update
    await queryClient.cancelQueries({ queryKey: ['user', id] });
    
    // Snapshot the previous value
    const previousUser = queryClient.getQueryData(['user', id]);
    
    // Optimistically update the cache
    queryClient.setQueryData(['user', id], (old) => ({
      ...old,
      ...updates,
    }));
    
    // Return context with previous value for rollback
    return { previousUser };
  },
  
  // Rollback on error
  onError: (error, { id }, context) => {
    // Restore previous value on error
    if (context?.previousUser) {
      queryClient.setQueryData(['user', id], context.previousUser);
    }
  },
  
  // Always refetch after success or error
  onSettled: (data, error, { id }) => {
    queryClient.invalidateQueries({ queryKey: ['user', id] });
  },
});

Mutation Cache Management

Direct access to mutation cache for advanced scenarios.

/**
 * Resume all paused mutations
 * Useful after network reconnection
 * @returns Promise that resolves when all mutations are resumed
 */
resumePausedMutations(): Promise<unknown>;

/**
 * Get the mutation cache instance
 * @returns The mutation cache
 */
getMutationCache(): MutationCache;

/**
 * Check how many mutations are currently running
 * @param filters - Optional filters to narrow the count
 * @returns Number of pending mutations
 */
isMutating(filters?: MutationFilters): number;

Usage Examples:

// Resume paused mutations after reconnection
await queryClient.resumePausedMutations();

// Check if any mutations are running
const mutationCount = queryClient.isMutating();
if (mutationCount > 0) {
  console.log(`${mutationCount} mutations currently running`);
}

// Check specific mutations
const userMutationCount = queryClient.isMutating({
  mutationKey: ['user'],
});

// Access mutation cache directly
const mutationCache = queryClient.getMutationCache();
const allMutations = mutationCache.getAll();
console.log('All mutations:', allMutations);

Scoped Mutations

Managing concurrent mutations with scoping to prevent conflicts.

interface MutationObserverOptions<T> {
  /**
   * Scope configuration for limiting concurrent mutations
   * Only one mutation with the same scope ID can run at once
   */
  scope?: {
    id: string;
  };
}

Usage Examples:

// Scoped mutations - only one per user at a time
const createScopedUserMutation = (userId) => new MutationObserver(queryClient, {
  mutationFn: async (updates) => {
    const response = await fetch(`/api/users/${userId}`, {
      method: 'PATCH',
      body: JSON.stringify(updates),
    });
    return response.json();
  },
  scope: {
    id: `user-${userId}`, // Only one mutation per user
  },
});

// Global scope - only one mutation of this type at a time
const globalMutation = new MutationObserver(queryClient, {
  mutationFn: async (data) => {
    // Critical operation that should not run concurrently
    const response = await fetch('/api/critical-operation', {
      method: 'POST',
      body: JSON.stringify(data),
    });
    return response.json();
  },
  scope: {
    id: 'critical-operation',
  },
});

Side Effects and Invalidation

Common patterns for handling side effects after mutations.

// Complex side effects with multiple invalidations
const complexMutation = new MutationObserver(queryClient, {
  mutationFn: async (data) => {
    const response = await fetch('/api/complex-operation', {
      method: 'POST',
      body: JSON.stringify(data),
    });
    return response.json();
  },
  
  onSuccess: async (data, variables) => {
    // Invalidate multiple related queries
    await Promise.all([
      queryClient.invalidateQueries({ queryKey: ['users'] }),
      queryClient.invalidateQueries({ queryKey: ['posts', data.userId] }),
      queryClient.invalidateQueries({ queryKey: ['notifications'] }),
    ]);
    
    // Update specific cached data
    queryClient.setQueryData(['user', data.userId], (old) => ({
      ...old,
      lastActivity: new Date().toISOString(),
    }));
    
    // Prefetch related data
    await queryClient.prefetchQuery({
      queryKey: ['user-stats', data.userId],
      queryFn: () => fetch(`/api/users/${data.userId}/stats`).then(r => r.json()),
    });
  },
  
  onError: (error, variables) => {
    // Handle specific error types
    if (error.status === 409) {
      // Conflict - refresh conflicting data
      queryClient.invalidateQueries({ queryKey: ['conflicts'] });
    } else if (error.status >= 500) {
      // Server error - maybe retry later
      console.error('Server error, consider retrying:', error);
    }
  },
});

Core Types

type MutationKey = ReadonlyArray<unknown>;

type MutationFunction<TData = unknown, TVariables = void> = (variables: TVariables) => Promise<TData>;

type MutationStatus = 'idle' | 'pending' | 'success' | 'error';

type RetryValue<TError> = boolean | number | ((failureCount: number, error: TError) => boolean);

type RetryDelayValue<TError> = number | ((failureCount: number, error: TError, mutation: Mutation) => number);

type ThrowOnError<TData, TError, TVariables, TContext> = 
  | boolean 
  | ((error: TError, variables: TVariables, context: TContext | undefined) => boolean);

type NetworkMode = 'online' | 'always' | 'offlineFirst';

interface MutationMeta extends Record<string, unknown> {}

interface MutationFilters {
  mutationKey?: MutationKey;
  exact?: boolean;
  predicate?: (mutation: Mutation) => boolean;
  status?: MutationStatus;
}

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