Mutation Concepts
Purpose and Scope
Mutation in SWR is the set of mechanisms that let an application change cached data, start a write request, or force a revalidation outside the normal read lifecycle. In read-only fetching, a component calls the core hook and SWR keeps the view fresh through cache lookup and revalidation. Mutation adds a deliberate write path: a user action can place a new value in the cache, ask SWR to fetch again, run a remote operation, or display an optimistic result while the server request is still in flight. This page explains the conceptual difference between local mutation and remote mutation, then connects those concepts to the public mutation hook, type options, tests, and optimistic UI example.
Sources: src/mutation/index.ts, src/mutation/types.ts, test/use-swr-local-mutation.test.tsx, test/use-swr-remote-mutation.test.tsx, examples/optimistic-ui/README.md
Local mutation is cache-oriented. It is useful when the application already knows the next value, wants to share local state through the SWR cache, or needs to manually revalidate an existing resource. The local mutation tests show all three patterns: calling the configured mutate function with only a key triggers revalidation, calling it with a key and value writes to the cache before the fetch completes, and using a hook without a fetcher turns SWR into a small shared-state store between components. In each case, the key is still the unit of identity, so all components reading the same serialized key observe the same update.
Remote mutation is request-oriented. It is represented by the mutation subpackage and the exported hook commonly imported from the mutation entrypoint. Instead of automatically requesting data during render, the hook returns a trigger function that starts the mutation only when the application calls it. The remote mutation tests assert that the page initially renders a pending state, the fetcher has not run yet, and a button click starts the operation. This delayed start makes the hook fit form submissions, button actions, deletes, creates, and other user-initiated writes where rendering a component should not automatically send a write request.
Relevant Source Files
- src/mutation/index.ts — Implements the remote mutation hook by reading the global mutate function from SWR configuration, serializing keys, tracking in-flight mutation timestamps, managing data, error, and mutating state, and exposing trigger and reset behavior.
- src/mutation/types.ts — Defines the public TypeScript contract for mutation fetchers, trigger overloads, mutation configuration, optimistic data, rollback behavior, cache population, revalidation, and callbacks.
- test/use-swr-local-mutation.test.tsx — Exercises local cache mutation, programmatic revalidation, deduping behavior after mutation, async cache writes, and SWR-backed local state sharing with no fetcher.
- test/use-swr-remote-mutation.test.tsx — Exercises remote mutation triggering, returned data, argument passing, success and error callbacks, and the visible pending-to-result flow for the mutation hook.
- examples/optimistic-ui/README.md — Documents the optimistic UI example, where cached data is mutated immediately and then revalidated with the API.
Core Primitives
A mutation key identifies the cache entry and the logical resource being written. The remote hook serializes the current key before calling the global mutate function, and it throws if the serialized key is missing. That behavior matters for conditional mutation: a false, null, undefined, or otherwise unresolved key should not accidentally create a write operation. The fetcher receives the resolved key rather than the serialized string, so array and function keys can still carry structured arguments into the mutation request. The tests verify this by using an array key and expecting the fetcher to receive that array along with the trigger argument.
Sources: src/mutation/index.ts, src/mutation/types.ts, test/use-swr-remote-mutation.test.tsx
A mutation fetcher is different from a read fetcher because it receives an options object containing the extra argument supplied to the trigger. The type definition models this with a fetcher options object whose arg property is read-only. The trigger overloads then adapt to the presence or absence of extra arguments. If a mutation requires a payload, the trigger can be typed to require it; if a mutation has no payload, the trigger can be called without a meaningful argument. This keeps the ergonomic button-click API while preserving useful TypeScript checks for forms and domain-specific mutation helpers.
The mutation response is also stateful. The implementation stores data, error, and is-mutating state in a dependency-aware state helper, and the tests observe these transitions through rendered output. Before the user calls trigger, data is absent and the interface can show a pending state. When the request succeeds, the hook stores the returned data and calls the success callback. When it fails, the hook stores the error, calls the error callback, and, by default, rethrows so the trigger caller can also handle the failure locally. This split lets a component both render an error state and run imperative error handling.
Local Mutation and Cache Writes
Local mutation is the most direct way to change what SWR readers see. In the local mutation tests, a component captures the configured mutate function from the configuration hook, renders data from a normal SWR read, and then calls mutate in response to an action. Calling mutate with only the key programmatically revalidates the resource. Calling mutate with a value updates the cache and then revalidates, so the view participates in the same stale-then-fresh lifecycle as normal fetching. The tests intentionally set deduping intervals to show that explicit mutation can request fresh data even when automatic deduplication would otherwise suppress duplicate reads.
Sources: test/use-swr-local-mutation.test.tsx
The shared-state test is a useful mental model for local mutation because it removes remote fetching from the picture. A helper creates a key namespace, calls the read hook with fallback data, and treats the bound mutate function as a setter. Two independent pieces of state, a name and a job, are updated from one click handler, and the rendered text changes after the mutation. This does not mean every local UI state should move into SWR, but it demonstrates that the cache is the synchronization point. If multiple components subscribe to the same key, local mutation is how an event in one place becomes visible in another.
Local mutation also interacts with request deduplication. One test mounts two readers for the same key, then triggers revalidation through mutate while a deduping interval is configured. The expected result increments once, not once per reader. Conceptually, mutate invalidates and revalidates a resource, while the cache and request coordinator prevent unnecessary duplicate network work. This is important when a page has several panels reading the same resource: a save button can invalidate the shared key without each panel independently launching its own duplicate follow-up request.
Remote Mutation Execution Flow
The remote mutation hook is implemented as a wrapper around the same global mutate capability, but it adds a trigger-first interface and local mutation state. On render, it stores the key, fetcher, and configuration in refs so the trigger callback can remain stable. When trigger runs, it serializes the latest key, validates that a fetcher and key exist, merges defaults with hook-level and call-level options, records a timestamp, and sets is-mutating. It then calls the global mutate function with the serialized key and a promise produced by the mutation fetcher. The fetcher receives the resolved key and the trigger argument packaged as an arg option.
Sources: src/mutation/index.ts
The default remote mutation options are intentionally conservative about cache writes. The implementation merges a default configuration where cache population is disabled and errors are thrown, then overlays hook-level configuration and trigger-level configuration. Disabling cache population by default avoids assuming that the response from a write request is the same shape as the cached read data. A create endpoint might return only an identifier, a delete endpoint might return a status, and an update endpoint might return a different projection than the list currently cached. When the response should become cache data, the caller can opt in with the populate-cache option or provide a transform function.
Timestamps protect the hook from out-of-order or discarded mutation results. The implementation records the start time of each trigger in a ref and uses that value to ignore earlier results after a newer trigger or reset has occurred. If a reset happens after a mutation starts, that older mutation should not repopulate the component state when it resolves. If two triggers race, the later one becomes the state owner. This is a subtle but important UI guarantee: a slow response from an earlier click should not overwrite a newer user action merely because the network completed in an inconvenient order.
Optimistic UI, Rollback, and Revalidation
Optimistic UI is the user-facing pattern where the interface updates before the server confirms the write. The optimistic UI example describes the pattern as mutating cached data immediately and then triggering revalidation with the API. In practical terms, a user action can apply a predicted value to the cache so the UI feels instant, while SWR still performs the remote request and follow-up validation. This fits operations such as toggling a like, editing a title, or appending a new item to a list, where the expected result is usually known from the submitted input.
Sources: examples/optimistic-ui/README.md, src/mutation/types.ts
The mutation type configuration names the main controls for this pattern. Optimistic data can be a concrete replacement value or a function of the current cached data, which is useful for list updates and counters. Rollback on error can be a boolean or a predicate, allowing callers to restore the previous cache state for real failures while optionally keeping optimistic data for errors they choose to ignore. Revalidation can be a boolean or a function of returned data and key arguments, so an application can decide whether the post-write state is already authoritative or whether another read request should verify the cache.
Cache population is separate from optimistic data. Optimistic data describes what to show before the remote request completes; populate-cache describes whether and how the remote result should become the new cached read value. Keeping these concepts separate prevents accidental cache shape mismatches. A mutation can optimistically update a local list, roll back if the request fails, skip cache population because the response is only a status, and still revalidate the original key afterward. Alternatively, a mutation can transform the returned record into the existing cached list and avoid an extra revalidation when the server response is complete enough.
Callback and Error Semantics
Success and error callbacks are available at both hook creation and trigger call sites. The remote mutation tests cover a hook configured with a success callback and another case where success handling is supplied when trigger is called. The implementation calls the selected success callback only after the mutation result is still current according to the timestamp guard. On failure, it updates error state, calls the error callback, and respects the throw-on-error option. The default is to throw, which lets callers write imperative flows such as awaiting trigger in a submit handler and handling rejected promises near the UI action.
Sources: src/mutation/index.ts, src/mutation/types.ts, test/use-swr-remote-mutation.test.tsx
The reset function complements error handling by clearing data, error, and is-mutating state and advancing the discard timestamp. Conceptually, reset returns the mutation hook to its initial local state without necessarily changing the underlying read cache. This is useful for dialog and form flows where a previous submission result should disappear when the user closes or reopens the form. Because reset also invalidates older in-flight state updates for the hook instance, a late-arriving response from a discarded operation will not immediately reintroduce stale success or error state into the component.
Practical Guidance
Use local mutation when the primary operation is cache management: revalidate a key after an external event, place a known value into the cache, or share local state through SWR keys. Use the remote mutation hook when the primary operation is an imperative write request: submit a form, delete an item, or send a command that should not run during render. In many real features, the two ideas combine. A remote mutation trigger may use optimistic data to locally update the relevant read cache, then roll back or revalidate after the request settles. The important design decision is which key represents the resource that readers should observe.
A typical optimistic flow starts by choosing the same key used by the read hook, computing optimistic data from the current cache, calling the mutation trigger from an event handler, and deciding whether the server response should populate the cache. If the write endpoint returns complete canonical data, populate the cache or transform the result. If it returns only an acknowledgement, prefer revalidation so the next read comes from the normal fetcher. If failures should undo visible changes, enable rollback on error. If the UI has its own success or failure notifications, use callbacks or await the trigger result in the event handler.
Related Pages
After this concept page, read the API references for mutate and the remote mutation hook to see exact signatures, options, and typed overloads. The cache and provider page explains why keys and cache scope determine which components receive local updates. The revalidation strategies page explains how manual mutation-triggered revalidation fits alongside focus, reconnect, interval, and mount revalidation. For hands-on patterns, continue with the optimistic UI examples, including the Immer variant, where immutable update helpers make optimistic list transformations easier to write and review.