Signals for managing, caching and syncing asynchronous and remote data in Angular
—
Mutation functionality for server-side effects with optimistic updates, error handling, and automatic query invalidation using Angular signals.
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;
}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';
}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>;
}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>;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;
}@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'] });
}
}));
}@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'] });
}
}));
}@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
}
}// 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