Mobx Query
Mutations

How It Works

Understanding how mutations work in mobx-query — from optimistic updates to entity state management and automatic cache invalidation.

This page explains the high-level architecture behind mutations in mobx-query. Understanding these mechanics helps you choose the right mutation class and configure the right strategies for your use case.

Mutation Architecture

Mutations in mobx-query handle write operations — creating new entities, updating existing ones, and deleting records. Every mutation class integrates with TanStack Query's mutation cache and provides built-in optimistic update behavior with configurable strategies.

mobx-query provides four mutation classes, organized by where they're defined and what they operate on:

Store Mutations

Defined on stores — they operate on the entity collection level.

ClassPurpose
CreateMutationInsert a new entity into the collection
BatchUpdateMutationUpdate multiple existing entities at once

Entity Mutations

Defined on entity instances — they operate on a specific entity.

ClassPurpose
UpdateMutationModify an existing entity's fields
DeleteMutationRemove an entity

The Optimistic Update Pattern

All mutations in mobx-query follow the same optimistic update pattern:

1. Apply changes locally (instant UI update)
2. Set entity state → 'pending'
3. Run mutationFn asynchronously (server request)
4. On success → confirm changes, invalidate queries
5. On error → rollback or keep, based on strategy

This means your UI updates immediately when the user performs an action — without waiting for the server. The server request happens in the background, and if it fails, the library handles the rollback automatically.

Entity State Transitions

All mutations update the entity.state property through the OptimisticMutationStrategy:

confirmed ──── mutate() ────► pending

                        ┌────────┴────────┐
                        ▼                  ▼
                    confirmed           failed
                  (on success)        (on error)
StateMeaning
confirmedDefault state. The entity is in sync with the server (or no mutation is in progress).
pendingA mutation (CreateMutation, UpdateMutation, or DeleteMutation) is currently in flight.
failedThe most recent mutation failed.

Use entity.state in your UI to show loading spinners, disable buttons, or display error indicators:

const FolderActions = observer(({ folder }: { folder: Folder }) => {
  return (
    <div>
      <button
        onClick={() => folder.deleteMutation.mutate()}
        disabled={folder.state === "pending"}
      >
        {folder.state === "pending" ? "Deleting..." : "Delete"}
      </button>
      {folder.state === "failed" && (
        <span className="error">Operation failed. Please try again.</span>
      )}
    </div>
  );
});

Mutation Key Construction

Similar to queries, mutation keys are constructed with prefixes for organized cache management:

[ EntityName, MutationPrefix, ...entityId? ]

Each mutation class uses a unique prefix:

ClassPrefix
CreateMutation__mutation__create__
UpdateMutation__mutation__update__
DeleteMutation__mutation__delete__
BatchUpdateMutation__mutation__batch__update__

Entity mutations (UpdateMutation, DeleteMutation) also append the entity's id, creating a unique mutation key per entity instance. This prevents duplicate mutations on the same entity and enables TanStack Query devtools inspection.

Lifecycle Callbacks

All mutation classes support lifecycle callbacks that run at specific points during the mutation:

CallbackWhenReceives
onMutateBefore mutationFn runsEntity + mutation context
onSuccessAfter mutationFn resolvesEntity + context + onMutate result
onErrorAfter mutationFn rejectsError + entity + context
onSettledAfter either success or errorEntity + error (or null) + context
readonly createFolder = new CreateMutation<CreateFolderInput, typeof Folder>({
  entity: Folder,
  mutationFn: async (input, entity) => {
    await fetch('/api/folders', {
      method: 'POST',
      body: JSON.stringify({ id: entity.id, ...input }),
      headers: { 'Content-Type': 'application/json' },
    })
  },
  invalidationStrategy: "all-entity-queries",
  onMutate: (input, entity) => {
    console.log('Creating folder:', entity.id);
  },
  onSuccess: (input, entity) => {
    console.log('Folder created successfully:', entity.id);
    // Navigate to the new folder, show a toast, etc.
  },
  onError: (error) => {
    console.error('Failed to create folder:', error);
  },
});

Lifecycle callbacks run after the built-in optimistic strategy logic. For example, onError runs after the entity has already been rolled back (if using 'rollback' strategy). This means you can safely inspect entity.state and entity.isDirty in callbacks and they'll reflect the post-strategy state.

useMutation() vs mutate()

Entity mutations (UpdateMutation, DeleteMutation) expose two ways to trigger them:

useMutation() — React Hook

Registers the mutation with TanStack Query's React lifecycle and mutation cache. Gives you devtools visibility and component-scoped cleanup:

const save = folder.updateMutation.useMutation();
// Returns a trigger function to call in event handlers

mutate() — Direct Call

Bypasses React but still runs through TanStack Query's mutation cache internally via runSyncMutation. Useful for entity actions and event handlers outside of React:

@action onPinFolder() {
  this.isPinned = !this.isPinned;
  this.updateMutation.mutate();
}

CreateMutation and BatchUpdateMutation only support the useMutation() hook, since they are store-level operations that require React's lifecycle.

On this page