Optimistic Strategies
Configure how mutations handle cache invalidation on success and entity rollback on error — globally and per-mutation.
Every mutation in mobx-query uses an OptimisticMutationStrategy that controls two critical behaviors:
- Invalidation strategy — which queries to refetch after the mutation succeeds.
- Error strategy — what happens to the entity's local state if the mutation fails.
Both can be configured globally on the MQClient and overridden per-mutation.
Invalidation Strategies
After a successful mutation, mobx-query needs to decide which queries to refetch so the UI stays consistent with the server. The invalidationStrategy option controls this behavior.
type OptimisticMutationInvalidationStrategy =
| "all-queries"
| "all-entity-queries"
| "referenced-queries"
| "none";'all-queries'
Invalidates every query across the entire application — regardless of entity type. This is the broadest possible strategy.
invalidationStrategy: "all-queries";Under the hood, this calls:
queryClient.invalidateQueries();When to use: For mutations that have cross-entity side effects (e.g. deleting a folder also affects document queries). Use sparingly — this triggers refetches for every active query.
'all-entity-queries'
Invalidates every query that returns the same entity type as the mutation. This is the most common choice for creates and deletes.
// After creating a folder, all Folder queries are invalidated:
// - foldersPreviewQuery
// - folderByIdQuery
// - any other query using entity: Folder
invalidationStrategy: "all-entity-queries";Under the hood, this calls:
queryClient.invalidateQueries({ queryKey: ["Folder"] });Since all Folder queries have 'Folder' as their first key segment (see Query Key Construction), this matches every one of them.
When to use: For mutations that change the total set of entities (creates, deletes) or modify fields that could affect sort order or filtering in other queries.
'referenced-queries'
Invalidates only the queries that directly reference the mutated entity — determined by the entity's queryHashes set.
invalidationStrategy: "referenced-queries";For example, if a Folder entity appears in foldersPreviewQuery and folderByIdQuery, only those two queries are refetched — not a recentFoldersQuery that doesn't include this particular folder.
When to use: When a mutation only affects the mutated entity itself (e.g. renaming a folder) and you want to minimize unnecessary refetches.
CreateMutation does not support 'referenced-queries'. A newly created
entity doesn't exist in any query's cache yet, so there are no "referenced
queries" to invalidate. Use 'all-entity-queries' or 'all-queries' instead.
'none'
Skips all invalidation. No queries are refetched after the mutation succeeds.
invalidationStrategy: "none";When to use: For high-frequency mutations where you trust the optimistic state (e.g. auto-saving document content on every keystroke). You might manually invalidate later or rely on the user navigating away and back.
Comparing Strategies
┌──────────────────────────────────────────────────────────────────────────────┐
│ Mutation succeeds │
├───────────────────┬────────────────────┬────────────────────┬────────────────┤
│ 'all-queries' │'all-entity-queries'│'referenced-queries'│ 'none' │
├───────────────────┼────────────────────┼────────────────────┼────────────────┤
│ Invalidate ALL │ Invalidate ALL │ Invalidate ONLY │ No invalidation│
│ queries in the │ queries for this │ queries that │ │
│ entire app │ entity type │ reference this │ │
│ │ │ specific entity │ │
├───────────────────┼────────────────────┼────────────────────┼────────────────┤
│ Broadest, nuclear │ Broad, safe for │ Targeted, │ Manual control │
│ │ entity collections │ efficient │ │
└───────────────────┴────────────────────┴────────────────────┴────────────────┘Error Strategies
When a mutation fails, you can configure what happens to the entity's local state:
type OptimisticMutationErrorStrategy = "rollback" | "keep";'rollback'
Reverts the entity to its previous state:
UpdateMutation: callsentity.reset(), restoring all@observable accessorfields to their server-confirmed values.CreateMutation: removes the entity from theEntityCollectionentirely.DeleteMutation: removes the entity's ID fromdeletedRecordIds, making it reappear in all query results.BatchUpdateMutation: callsentity.reset()on each dirty entity in the batch.
readonly updateMutation = new UpdateMutation({
entity: Folder,
instance: this,
mutationFn: async () => { /* ... */ },
errorStrategy: 'rollback', // default — reverts on error
});'keep'
Preserves the optimistic state even after an error. The entity keeps its locally modified values and state is set to 'failed'.
readonly updateMutation = new UpdateMutation({
entity: Folder,
instance: this,
mutationFn: async () => { /* ... */ },
errorStrategy: 'keep', // keep local changes, let the user retry
});When to use 'keep': When you want to let the user correct the issue and retry without losing their input. For example, a form submission that fails due to a network error — having the values disappear is a worse UX than showing an error with a retry button.
Combining Error Strategy with invalidateOnError
The invalidateOnError option controls whether the invalidation strategy also runs when a mutation fails. This is independent of the error strategy:
errorStrategy | invalidateOnError | Behavior on error |
|---|---|---|
'rollback' | false | Reverts entity, no query refetch |
'rollback' | true | Reverts entity and refetches queries (most conservative) |
'keep' | false | Keeps local changes, no query refetch |
'keep' | true | Keeps local changes but still refetches (useful for syncing) |
Global Defaults
You can configure the default strategies for all mutations when initializing MQClient:
const client = new MQClient({
context: { queryClient },
entities: [Folder, Document],
rootStore: () => new RootStore(),
invalidationStrategy: "referenced-queries", // default for all mutations
errorStrategy: "rollback", // default for all mutations
invalidateOnError: true, // default for all mutations
});Built-in Defaults
If no global configuration is specified, these defaults apply:
| Option | Default |
|---|---|
invalidationStrategy | 'referenced-queries' |
errorStrategy | 'rollback' |
invalidateOnError | true |
Per-Mutation Override
Individual mutations can override the global default:
readonly updateMutation = new UpdateMutation({
entity: Folder,
instance: this,
mutationFn: async () => { /* ... */ },
invalidationStrategy: 'none', // overrides the global default
errorStrategy: 'keep', // overrides the global default
})The override resolution order is:
Per-mutation option → MQClient global option → Built-in defaultStrategy Recipes
Here are common strategy combinations for real-world scenarios:
Form Save (default)
The entity is edited in a form. On error, revert to the previous state and show an error message.
readonly updateMutation = new UpdateMutation({
entity: Folder,
instance: this,
mutationFn: async () => { /* ... */ },
invalidationStrategy: 'referenced-queries',
errorStrategy: 'rollback',
})Auto-Save
Content is saved automatically on every change (e.g. a document editor). No refetch needed, and local changes should be preserved on error so the user can retry.
readonly autoSaveMutation = new UpdateMutation({
entity: Document,
instance: this,
mutationFn: async () => { /* ... */ },
invalidationStrategy: 'none',
errorStrategy: 'keep',
})Critical Write
An operation that affects many entities (e.g. publishing a document changes its status across multiple queries). Refetch all related queries and rollback on error.
readonly publishMutation = new UpdateMutation({
entity: Document,
instance: this,
mutationFn: async () => { /* ... */ },
invalidationStrategy: 'all-entity-queries',
errorStrategy: 'rollback',
})Quick Toggle
A simple boolean toggle (e.g. pin/unpin). Changes apply instantly, failures rollback, only the queries referencing this entity need updating.
@action onPinFolder() {
this.isPinned = !this.isPinned;
this.updateMutation.mutate();
}
readonly updateMutation = new UpdateMutation({
entity: Folder,
instance: this,
mutationFn: async () => { /* ... */ },
invalidationStrategy: 'referenced-queries',
errorStrategy: 'rollback',
})Destructive Delete with Full Sync
A delete that might affect other entity types (e.g. deleting a folder removes its documents). Refetch everything.
readonly deleteMutation = new DeleteMutation({
entity: Folder,
instance: this,
mutationFn: async () => { /* ... */ },
invalidationStrategy: 'all-queries',
errorStrategy: 'rollback',
})