SoulCache

Mutation

Learn about SoulCache's mutation system for data modifications

Mutation System

The Mutation System handles data modifications with automatic cache invalidation and optimistic updates.

Core Concepts

Mutations are operations that modify data on the server:

  • Create
  • Update
  • Delete
  • Any side-effect operation

Usage

Basic Mutation

const result = await client.mutate({
  mutationFn: async (variables) => {
    const response = await fetch('/api/users', {
      method: 'POST',
      body: JSON.stringify(variables),
    });
    return response.json();
  },
  variables: { name: 'John' },
});

With Callbacks

const result = await client.mutate({
  mutationFn: createUser,
  variables: { name: 'John' },
  onMutate: (variables) => {
    // Optimistic update - return context
    client.setQueryData(['users'], (old) => [...old, variables]);
    return { previous: client.getQueryData(['users']) };
  },
  onSuccess: (data, variables) => {
    console.log('User created:', data);
  },
  onError: (error, variables) => {
    console.error('Failed:', error);
  },
  onSettled: (data, error, variables) => {
    // Always runs
    client.invalidateQueries(['users']);
  },
});

Callback errors

Mutation callbacks are isolated from the mutation outcome:

  • An onSuccess/onSettled that throws cannot turn a success into an error — the mutation resolves with its data and the state stays success.
  • An onError that throws cannot replace the original mutation error and cannot prevent onSettled from running.
  • An onMutate that throws is treated as a mutation error (onError and onSettled still run).
  • onSettled runs on every success/error, even when other callbacks throw. (Cancelled mutations are the exception and do not fire onSettled.)
  • Callbacks are expected to be synchronous (they are typed to return void). An async callback that rejects rather than throwing synchronously is not isolated and its rejection is not handled.

With Retry

Retry

The retry and retryDelay parameters are accepted by QueryClient.mutate() but are not applied — mutate() performs a single attempt. Use MutationEntry.mutateWithRetry() directly for retry behavior.

// Using MutationEntry directly for retry
const entry = client.getMutationCache().create({
  mutationId: 'mut-1',
  mutationFn: createUser,
});
 
const result = await entry.mutateWithRetry(
  { name: 'John' },
  3,   // maxRetries
  1000 // retryDelay (ms)
);

Optimistic Updates

Optimistically update cache before server confirms:

await client.mutate({
  mutationFn: updateUser,
  variables: { id: 1, name: 'Jane' },
  onMutate: (variables) => {
    // Snapshot previous value
    const previous = client.getQueryData(['users']);
 
    // Optimistically update
    client.setQueryData(['users'], (old) =>
      old.map(user => user.id === variables.id ? { ...user, ...variables } : user)
    );
 
    // Return context with snapshot
    return { previous };
  },
  onError: (error, variables) => {
    // Rollback on error (access context via closure)
    client.setQueryData(['users'], previousData);
  },
  onSettled: () => {
    // Refetch after error or success
    client.invalidateQueries(['users']);
  },
});

Mutation States

Mutations go through these states:

StateDescription
idleNot active
pendingCurrently executing
successCompleted successfully
errorFailed

MutationCache

Access the mutation cache directly:

const mutationCache = client.getMutationCache();
 
// Find mutations by status
const pendingMutations = mutationCache.findAll({ status: 'pending' });
 
// Clear all mutations
mutationCache.clear();

Best Practices

Mutation Guidelines

  • Always invalidate related queries — Keep cache consistent
  • Use optimistic updates carefully — Rollback on errors
  • Handle loading states — Show feedback to users
  • Clean up side effects — Use onSettled for cleanup

On this page