CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-tanstack--angular-query-experimental

Signals for managing, caching and syncing asynchronous and remote data in Angular

Pending
Overview
Eval results
Files

mutation-management.mddocs/

Mutation Management

Mutation functionality for server-side effects with optimistic updates, error handling, and automatic query invalidation using Angular signals.

Capabilities

Inject Mutation

Creates a mutation that can be executed to perform server-side effects like creating, updating, or deleting data.

/**
 * Injects a mutation: an imperative function that can be invoked which typically performs server side effects.
 * Unlike queries, mutations are not run automatically.
 * @param injectMutationFn - A function that returns mutation options
 * @param options - Additional configuration including custom injector
 * @returns The mutation result with signals and mutate functions
 */
function injectMutation<TData, TError, TVariables, TContext>(
  injectMutationFn: () => CreateMutationOptions<TData, TError, TVariables, TContext>,
  options?: InjectMutationOptions
): CreateMutationResult<TData, TError, TVariables, TContext>;

Usage Examples:

import { injectMutation, QueryClient } from "@tanstack/angular-query-experimental";
import { Component, inject } from "@angular/core";
import { HttpClient } from "@angular/common/http";

@Component({
  selector: 'app-user-form',
  template: `
    <form (ngSubmit)="handleSubmit()">
      <input [(ngModel)]="name" placeholder="Name" />
      <input [(ngModel)]="email" placeholder="Email" />
      <button 
        type="submit" 
        [disabled]="createUserMutation.isPending()"
      >
        {{ createUserMutation.isPending() ? 'Creating...' : 'Create User' }}
      </button>
    </form>
    
    <div *ngIf="createUserMutation.isError()">
      Error: {{ createUserMutation.error()?.message }}
    </div>
    
    <div *ngIf="createUserMutation.isSuccess()">
      User created: {{ createUserMutation.data()?.name }}
    </div>
  `
})
export class UserFormComponent {
  #http = inject(HttpClient);
  #queryClient = inject(QueryClient);
  
  name = '';
  email = '';
  
  // Basic mutation
  createUserMutation = injectMutation(() => ({
    mutationFn: (userData: CreateUserRequest) => 
      this.#http.post<User>('/api/users', userData),
    onSuccess: (newUser) => {
      // Invalidate and refetch users list
      this.#queryClient.invalidateQueries({ queryKey: ['users'] });
      // Optionally set data directly
      this.#queryClient.setQueryData(['user', newUser.id], newUser);
    },
    onError: (error) => {
      console.error('Failed to create user:', error);
    }
  }));
  
  // Mutation with optimistic updates
  updateUserMutation = injectMutation(() => ({
    mutationFn: (data: { id: number; updates: Partial<User> }) =>
      this.#http.patch<User>(`/api/users/${data.id}`, data.updates),
    onMutate: async (variables) => {
      // Cancel outgoing refetches
      await this.#queryClient.cancelQueries({ queryKey: ['user', variables.id] });
      
      // Snapshot previous value
      const previousUser = this.#queryClient.getQueryData<User>(['user', variables.id]);
      
      // Optimistically update
      this.#queryClient.setQueryData(['user', variables.id], (old: User) => ({
        ...old,
        ...variables.updates
      }));
      
      return { previousUser };
    },
    onError: (error, variables, context) => {
      // Rollback on error
      if (context?.previousUser) {
        this.#queryClient.setQueryData(['user', variables.id], context.previousUser);
      }
    },
    onSettled: (data, error, variables) => {
      // Always refetch after error or success
      this.#queryClient.invalidateQueries({ queryKey: ['user', variables.id] });
    }
  }));
  
  handleSubmit() {
    this.createUserMutation.mutate({
      name: this.name,
      email: this.email
    });
  }
}

interface User {
  id: number;
  name: string;
  email: string;
}

interface CreateUserRequest {
  name: string;
  email: string;
}

Mutation Options Interface

Comprehensive options for configuring mutation behavior.

interface CreateMutationOptions<TData, TError, TVariables, TContext> {
  /** Function that performs the mutation and returns a promise */
  mutationFn: MutationFunction<TData, TVariables>;
  /** Unique key for the mutation (optional) */
  mutationKey?: MutationKey;
  /** Called before mutation function is fired */
  onMutate?: (variables: TVariables) => Promise<TContext> | TContext;
  /** Called on successful mutation */
  onSuccess?: (data: TData, variables: TVariables, context: TContext) => Promise<void> | void;
  /** Called on mutation error */
  onError?: (error: TError, variables: TVariables, context: TContext | undefined) => Promise<void> | void;
  /** Called after mutation completes (success or error) */
  onSettled?: (data: TData | undefined, error: TError | null, variables: TVariables, context: TContext | undefined) => Promise<void> | void;
  /** Number of retry attempts on failure */
  retry?: boolean | number | ((failureCount: number, error: TError) => boolean);
  /** Delay function for retry attempts */
  retryDelay?: number | ((retryAttempt: number, error: TError) => number);
  /** Whether to throw errors instead of setting error state */
  throwOnError?: boolean | ((error: TError) => boolean);
  /** Time in milliseconds after which unused mutation data is garbage collected */
  gcTime?: number;
  /** Custom meta information */
  meta?: Record<string, unknown>;
  /** Whether to use network mode */
  networkMode?: 'online' | 'always' | 'offlineFirst';
}

Mutation Result Interface

Signal-based result object providing reactive access to mutation state.

interface CreateMutationResult<TData, TError, TVariables, TContext> {
  /** Function to trigger the mutation */
  mutate: CreateMutateFunction<TData, TError, TVariables, TContext>;
  /** Async function to trigger the mutation and return a promise */
  mutateAsync: CreateMutateAsyncFunction<TData, TError, TVariables, TContext>;
  /** Signal containing the mutation data */
  data: Signal<TData | undefined>;
  /** Signal containing any error that occurred */
  error: Signal<TError | null>;
  /** Signal containing the variables passed to the mutation */
  variables: Signal<TVariables | undefined>;
  /** Signal indicating if mutation is currently running */
  isPending: Signal<boolean>;
  /** Signal indicating if mutation completed successfully */
  isSuccess: Signal<boolean>;
  /** Signal indicating if mutation resulted in error */
  isError: Signal<boolean>;
  /** Signal indicating if mutation is idle (not yet called) */
  isIdle: Signal<boolean>;
  /** Signal containing mutation status */
  status: Signal<'idle' | 'pending' | 'error' | 'success'>;
  /** Signal containing current failure count */
  failureCount: Signal<number>;
  /** Signal containing failure reason */
  failureReason: Signal<TError | null>;
  
  // Type narrowing methods
  isSuccess(this: CreateMutationResult<TData, TError, TVariables, TContext>): this is CreateMutationResult<TData, TError, TVariables, TContext>;
  isError(this: CreateMutationResult<TData, TError, TVariables, TContext>): this is CreateMutationResult<TData, TError, TVariables, TContext>;
  isPending(this: CreateMutationResult<TData, TError, TVariables, TContext>): this is CreateMutationResult<TData, TError, TVariables, TContext>;
  isIdle(this: CreateMutationResult<TData, TError, TVariables, TContext>): this is CreateMutationResult<TData, TError, TVariables, TContext>;
}

Mutation Function Types

Type definitions for mutation functions.

type CreateMutateFunction<TData, TError, TVariables, TContext> = (
  variables: TVariables,
  options?: {
    onSuccess?: (data: TData, variables: TVariables, context: TContext) => void;
    onError?: (error: TError, variables: TVariables, context: TContext | undefined) => void;
    onSettled?: (data: TData | undefined, error: TError | null, variables: TVariables, context: TContext | undefined) => void;
  }
) => void;

type CreateMutateAsyncFunction<TData, TError, TVariables, TContext> = (
  variables: TVariables,
  options?: {
    onSuccess?: (data: TData, variables: TVariables, context: TContext) => void;
    onError?: (error: TError, variables: TVariables, context: TContext | undefined) => void;
    onSettled?: (data: TData | undefined, error: TError | null, variables: TVariables, context: TContext | undefined) => void;
  }
) => Promise<TData>;

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

Options Configuration

Configuration interface for injectMutation behavior.

interface InjectMutationOptions {
  /**
   * The Injector in which to create the mutation.
   * If not provided, the current injection context will be used instead (via inject).
   */
  injector?: Injector;
}

Advanced Usage Patterns

Optimistic Updates

@Component({})
export class OptimisticUpdateComponent {
  #http = inject(HttpClient);
  #queryClient = inject(QueryClient);
  
  updateTodoMutation = injectMutation(() => ({
    mutationFn: (data: { id: number; completed: boolean }) =>
      this.#http.patch<Todo>(`/api/todos/${data.id}`, { completed: data.completed }),
    
    onMutate: async (variables) => {
      // Cancel any outgoing refetches so they don't overwrite optimistic update
      await this.#queryClient.cancelQueries({ queryKey: ['todos'] });
      
      // Snapshot previous value
      const previousTodos = this.#queryClient.getQueryData<Todo[]>(['todos']);
      
      // Optimistically update the cache
      this.#queryClient.setQueryData<Todo[]>(['todos'], (old = []) =>
        old.map(todo =>
          todo.id === variables.id
            ? { ...todo, completed: variables.completed }
            : todo
        )
      );
      
      return { previousTodos };
    },
    
    onError: (error, variables, context) => {
      // Rollback to previous state on error
      if (context?.previousTodos) {
        this.#queryClient.setQueryData(['todos'], context.previousTodos);
      }
    },
    
    onSettled: () => {
      // Always refetch after error or success to ensure we have latest data
      this.#queryClient.invalidateQueries({ queryKey: ['todos'] });
    }
  }));
}

Sequential Mutations

@Component({})
export class SequentialMutationsComponent {
  #http = inject(HttpClient);
  #queryClient = inject(QueryClient);
  
  createUserAndProfileMutation = injectMutation(() => ({
    mutationFn: async (userData: CreateUserData) => {
      // First create the user
      const user = await this.#http.post<User>('/api/users', {
        name: userData.name,
        email: userData.email
      }).toPromise();
      
      // Then create their profile
      const profile = await this.#http.post<Profile>('/api/profiles', {
        userId: user.id,
        bio: userData.bio,
        avatar: userData.avatar
      }).toPromise();
      
      return { user, profile };
    },
    
    onSuccess: ({ user, profile }) => {
      // Update cache with both entities
      this.#queryClient.setQueryData(['user', user.id], user);
      this.#queryClient.setQueryData(['profile', user.id], profile);
      this.#queryClient.invalidateQueries({ queryKey: ['users'] });
    }
  }));
}

Error Recovery

@Component({})
export class ErrorRecoveryComponent {
  #http = inject(HttpClient);
  
  retryableMutation = injectMutation(() => ({
    mutationFn: (data: any) => this.#http.post('/api/data', data),
    
    retry: (failureCount, error: any) => {
      // Retry up to 3 times, but only for specific errors
      if (failureCount < 3) {
        // Retry on network errors or 5xx server errors
        return !error.status || error.status >= 500;
      }
      return false;
    },
    
    retryDelay: (attemptIndex) => {
      // Exponential backoff with jitter
      const baseDelay = Math.min(1000 * 2 ** attemptIndex, 30000);
      return baseDelay + Math.random() * 1000;
    },
    
    onError: (error, variables, context) => {
      // Log error for monitoring
      console.error('Mutation failed after retries:', error);
      
      // Could show user-friendly error message
      this.showErrorMessage(error);
    }
  }));
  
  private showErrorMessage(error: any) {
    // Implementation for showing user-friendly errors
  }
}

Global Mutation Defaults

// Can be set up in app configuration
const queryClient = new QueryClient({
  defaultOptions: {
    mutations: {
      retry: 1,
      throwOnError: false,
      onError: (error) => {
        // Global error handling for all mutations
        console.error('Mutation error:', error);
      }
    }
  }
});

Install with Tessl CLI

npx tessl i tessl/npm-tanstack--angular-query-experimental

docs

index.md

infinite-queries.md

multi-query-operations.md

mutation-management.md

options-helpers.md

provider-setup.md

query-management.md

status-monitoring.md

tile.json