Skip to main content

Overview

Mutations are used to create, update, or delete data on the server. Unlike queries, mutations:
  • Are triggered manually (not automatically on mount)
  • Do not cache results by default
  • Can invalidate query cache to trigger refetches
  • Return promises for handling success/error states

Defining a Mutation Endpoint

Define mutations using build.mutation<ResultType, ArgType>() in your API definition:
import { createApi, fetchBaseQuery } from 'ngrx-rtk-query';

export interface Post {
  id: number;
  name: string;
}

export const postsApi = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: 'http://api.localhost.com' }),
  tagTypes: ['Posts'],
  endpoints: (build) => ({
    addPost: build.mutation<Post, Partial<Post>>({
      query: (body) => ({
        url: '/posts',
        method: 'POST',
        body,
      }),
      invalidatesTags: [{ type: 'Posts', id: 'LIST' }],
    }),
    updatePost: build.mutation<Post, Partial<Post>>({
      query: (data) => {
        const { id, ...body } = data;
        return {
          url: `/posts/${id}`,
          method: 'PUT',
          body,
        };
      },
      invalidatesTags: (result, error, { id }) => [{ type: 'Posts', id }],
    }),
    deletePost: build.mutation<{ success: boolean; id: number }, number>({
      query: (id) => ({
        url: `/posts/${id}`,
        method: 'DELETE',
      }),
      invalidatesTags: [{ type: 'Posts', id: 'LIST' }],
    }),
  }),
});

export const {
  useAddPostMutation,
  useUpdatePostMutation,
  useDeletePostMutation,
} = postsApi;

Basic Usage

The mutation hook returns a trigger function with state signals attached:
import { Component, signal } from '@angular/core';
import { useAddPostMutation } from './api';

@Component({
  selector: 'app-add-post',
  standalone: true,
  template: `
    <input 
      type="text" 
      [value]="postName()" 
      (input)="postName.set($any($event.target).value)" />
    
    <button 
      [disabled]="!postName() || addPost.isLoading()"
      (click)="addNewPost()">
      {{ addPost.isLoading() ? 'Adding...' : 'Add Post' }}
    </button>
    
    @if (addPost.isError()) {
      <p class="error">Error: {{ addPost.error() }}</p>
    }
  `,
})
export class AddPostComponent {
  addPost = useAddPostMutation();
  postName = signal('');
  
  addNewPost() {
    this.addPost({ name: this.postName() });
  }
}

Mutation Structure

The mutation hook returns a trigger function with signal properties:
const trigger = useAddPostMutation();

// Trigger function - call to execute the mutation
trigger({ name: 'New Post' });

// Signal properties - access mutation state
trigger.data();         // Mutation result data
trigger.isLoading();    // True while mutation is in progress
trigger.isSuccess();    // True if mutation succeeded
trigger.isError();      // True if mutation failed
trigger.error();        // Error object if mutation failed

// Utility methods
trigger.reset();        // Reset mutation state
trigger.originalArgs(); // The last arguments passed to trigger
Mutation hooks return the trigger function itself, with signals attached as properties (similar to lazy queries).

Mutation State Properties

PropertyTypeDescription
data()T | undefinedThe result data from the mutation
isLoading()booleantrue while the mutation is in progress
isSuccess()booleantrue if the mutation succeeded
isError()booleantrue if the mutation failed
isUninitialized()booleantrue if the mutation hasn’t been triggered yet
error()SerializedError | FetchBaseQueryErrorError object if mutation failed
originalArgs()ArgType | undefinedThe arguments passed to the mutation
reset()() => voidReset the mutation state

Unwrapping Promises

Use .unwrap() to handle the mutation result as a promise:
export class AddPostComponent {
  addPost = useAddPostMutation();
  postName = signal('');
  
  addNewPost() {
    this.addPost({ name: this.postName() })
      .unwrap()
      .then((newPost) => {
        console.log('Post created:', newPost);
        this.postName.set(''); // Clear form
      })
      .catch((error) => {
        console.error('Failed to create post:', error);
      });
  }
}

Async/Await Pattern

async addNewPost() {
  try {
    const newPost = await this.addPost({ name: this.postName() }).unwrap();
    console.log('Post created:', newPost);
    this.postName.set('');
  } catch (error) {
    console.error('Failed to create post:', error);
  }
}
Without .unwrap(), the promise will never reject. Always use .unwrap() to properly handle errors.

Resetting Mutation State

Use the reset() method to clear mutation state:
@Component({
  template: `
    <button (click)="addPost({ name: 'New' })">Add</button>
    <button (click)="addPost.reset()">Clear State</button>
    
    @if (addPost.isSuccess()) {
      <p>Post added successfully!</p>
    }
  `
})
export class AddPostComponent {
  addPost = useAddPostMutation();
}
Resetting clears the mutation state (data, error, isSuccess, etc.) but doesn’t affect the cache.

Common Use Cases

Creating Data

@Component({
  template: `
    <form (submit)="onSubmit()">
      <input 
        type="text" 
        [(ngModel)]="name" 
        name="name" 
        required />
      <button 
        type="submit" 
        [disabled]="addPost.isLoading()">
        {{ addPost.isLoading() ? 'Adding...' : 'Add Post' }}
      </button>
    </form>
    
    @if (addPost.isSuccess()) {
      <p class="success">Post created successfully!</p>
    }
  `
})
export class CreatePostComponent {
  addPost = useAddPostMutation();
  name = '';
  
  onSubmit() {
    this.addPost({ name: this.name })
      .unwrap()
      .then(() => {
        this.name = '';
        this.addPost.reset(); // Clear success message after delay
      });
  }
}

Updating Data

@Component({
  template: `
    <div class="post-editor">
      <input 
        type="text" 
        [ngModel]="postName()" 
        (ngModelChange)="postName.set($event)" />
      
      <button 
        [disabled]="updatePost.isLoading()"
        (click)="savePost()">
        {{ updatePost.isLoading() ? 'Saving...' : 'Save' }}
      </button>
    </div>
  `
})
export class EditPostComponent {
  id = input.required<number>();
  postName = signal('');
  
  updatePost = useUpdatePostMutation();
  
  savePost() {
    this.updatePost({ id: this.id(), name: this.postName() })
      .unwrap()
      .then(() => console.log('Post updated'));
  }
}

Deleting Data

import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
import { useDeletePostMutation } from './api';

@Component({
  template: `
    <button 
      class="delete-btn"
      [disabled]="deletePost.isLoading()"
      (click)="onDelete()">
      {{ deletePost.isLoading() ? 'Deleting...' : 'Delete' }}
    </button>
  `
})
export class PostDetailsComponent {
  id = input.required<number>();
  
  deletePost = useDeletePostMutation();
  #router = inject(Router);
  
  onDelete() {
    if (confirm('Are you sure?')) {
      this.deletePost(this.id())
        .unwrap()
        .then(() => this.#router.navigate(['/']))
        .catch((error) => console.error('Error deleting post:', error));
    }
  }
}

Fixed Cache Key

Use fixedCacheKey to share mutation state across multiple components:
export class Component1 {
  // Both components share the same mutation state
  addPost = useAddPostMutation({ fixedCacheKey: 'shared-add-post' });
}

export class Component2 {
  // Same fixedCacheKey = same state
  addPost = useAddPostMutation({ fixedCacheKey: 'shared-add-post' });
}
fixedCacheKey is useful for global loading indicators or shared form state across multiple views.

Mutation Options

Customize mutation behavior with options:
export class AddPostComponent {
  addPost = useAddPostMutation({
    // Share mutation state across components
    fixedCacheKey: 'add-post-mutation',
    
    // Select specific fields from mutation state
    selectFromResult: ({ data, isLoading }) => ({ data, isLoading }),
  });
}

selectFromResult

Transform the mutation state:
addPost = useAddPostMutation({
  selectFromResult: ({ data, isLoading, isError }) => ({
    post: data,
    loading: isLoading,
    error: isError,
  }),
});

// Access transformed state
addPost.post();
addPost.loading();
addPost.error();

Pessimistic vs Optimistic Updates

Pessimistic Updates (Default)

Wait for server response before updating UI:
deletePost(id: number) {
  this.deletePost(id)
    .unwrap()
    .then(() => {
      // Update UI after server confirms deletion
      this.router.navigate(['/']);
    });
}

Optimistic Updates

Update UI immediately, rollback on error (see Cache Invalidation for details):
deletePost: build.mutation<void, number>({
  query: (id) => ({ url: `/posts/${id}`, method: 'DELETE' }),
  
  async onQueryStarted(id, { dispatch, queryFulfilled }) {
    // Optimistically remove from cache
    const patchResult = dispatch(
      postsApi.util.updateQueryData('getPosts', undefined, (draft) => {
        return draft.filter(post => post.id !== id);
      })
    );
    
    try {
      await queryFulfilled;
    } catch {
      // Rollback on error
      patchResult.undo();
    }
  },
}),

TypeScript Type Signatures

Mutation Hook Type

type UseMutation<ResultType, ArgType> = (
  options?: UseMutationOptions<ResultType>
) => MutationTrigger<ResultType, ArgType>;

type MutationTrigger<ResultType, ArgType> = {
  // Trigger function
  (arg: ArgType): MutationActionCreatorResult<ResultType>;
  
  // Signal properties
  data: Signal<ResultType | undefined>;
  error: Signal<SerializedError | FetchBaseQueryError | undefined>;
  isUninitialized: Signal<boolean>;
  isLoading: Signal<boolean>;
  isSuccess: Signal<boolean>;
  isError: Signal<boolean>;
  
  // Utility properties
  originalArgs: Signal<ArgType | undefined>;
  reset: () => void;
};

Mutation Action Creator Result

interface MutationActionCreatorResult<T> {
  unwrap: () => Promise<T>;
  reset: () => void;
  arg: {
    originalArgs: unknown;
    fixedCacheKey?: string;
  };
  requestId: string;
}

Best Practices

// ✅ Good - Properly handle errors
addPost(data: Post) {
  this.addPost(data)
    .unwrap()
    .then(() => this.handleSuccess())
    .catch((error) => this.handleError(error));
}

// ❌ Bad - Errors won't be caught
addPost(data: Post) {
  this.addPost(data).then(() => this.handleSuccess());
}

Build docs developers (and LLMs) love