Remote Data Mutation
Overview
The Remote Data Mutation module provides a specialized hook and supporting types for performing controlled remote mutations (e.g., POST, PUT, DELETE, PATCH requests) in a React application. It integrates tightly with the SWR (stale-while-revalidate) data fetching ecosystem to enable optimistic UI updates, rollback on errors, and cache synchronization. This module addresses the challenges of managing remote state changes that require server communication while maintaining UI responsiveness and consistency.
Core Concepts
Mutation Hook: A React hook (
useSWRMutation) that allows manual triggering of data mutations associated with a specific resource key.Optimistic Updates: Supports preemptively updating cached data before the mutation completes, improving perceived UI responsiveness.
Rollback Mechanism: Automatically reverts optimistic updates if the mutation fails.
Cache Synchronization: Ensures the SWR cache is updated consistently after mutation success or failure.
State Management with Dependency Tracking: Tracks and exposes mutation state (
data,error,isMutating) with fine-grained re-render control.Configurable Behavior: Provides options for revalidation, cache population, optimistic data, rollback conditions, and callbacks for success/error handling.
Purpose and Problems Solved
Typical data fetching libraries focus on retrieving data, but many applications require changing data remotely and reflecting those changes immediately in the UI. Challenges include:
How to update UI immediately (optimistically) before the server confirms success.
How to prevent UI inconsistencies when mutations fail (rollback).
How to integrate mutation state cleanly with the existing caching and revalidation mechanisms.
How to provide a simple, declarative API to trigger mutations manually.
This module solves these by building on top of SWR’s cache and mutation mechanisms, providing a hook that exposes a trigger function to perform mutations with configurable options, state tracking, and cache management.
How the Module Works
useSWRMutation Hook
At the heart is the useSWRMutation hook, defined in src/mutation/index.ts. It wraps the core SWR hook with a custom middleware (mutation) to add mutation-specific functionality.
const useSWRMutation = withMiddleware(
useSWR,
mutation
)
Parameters:
key: The resource key identifying the data to mutate (should correspond to the key used byuseSWRfor cache consistency).fetcher: A function that performs the mutation request and returns the updated data or throws an error.config: Optional configuration for mutation behavior.
Returned Object:
trigger(arg, opts): Async function to perform the mutation with optional arguments and override options.reset(): Function to reset mutation state (data,error,isMutating).Accessors for
data,error, andisMutating, exposing current mutation state with dependency tracking to optimize re-renders.
Mutation Workflow
Triggering a Mutation
Callingtriggerexecutes the mutation fetcher with the provided argument(s). The mutation applies configuration defaults such as disabling cache population by default (populateCache: false) and throwing errors (throwOnError: true), which can be overridden.Timestamp-Based Mutation Ordering
To avoid race conditions from overlapping mutations, each mutation records a timestamp (mutationStartedAt). Mutations started earlier than the latest recorded timestamp (ditchMutationsUntilRef) are ignored in state updates to ensure only the latest mutation's result is applied.State Updates During Mutation
Before mutation: sets
isMutatingtotrue.On success: updates
data, clearserror, setsisMutatingtofalse, and optionally callsonSuccesscallback.On failure: updates
error, clearsdata, setsisMutatingtofalse, and optionally callsonErrorcallback.
Cache Mutation
The mutation internally calls SWR’smutatemethod to update the global cache associated with thekey, allowing cache synchronization and revalidation.Resetting State
Theresetmethod clears mutation state and increments the timestamp tracker to ignore ongoing mutations.
State Management with Dependency Tracking
Mutation state (data, error, isMutating) is managed using a custom hook useStateWithDeps (src/mutation/state.ts) which:
Keeps state in a mutable ref to avoid unnecessary re-renders.
Tracks which state properties are accessed during render to selectively trigger re-renders only when those properties change.
Uses React’s
startTransitionfor state updates to prioritize user interactions.
This efficient state management ensures UI components consuming mutation state re-render only when relevant parts change.
Mutation Types and Configurations (src/mutation/types.ts)
The module provides detailed TypeScript types to enable type safety and flexibility:
MutationFetcher: Defines the fetcher signature accepting the mutation key and an extra argument, returning data or a promise.SWRMutationConfiguration: Options include:revalidate: Whether or not to revalidate cache after mutation.populateCache: Whether and how to populate cache with mutation result (boolean or function).optimisticData: Data or updater function for optimistic UI updates.rollbackOnError: Condition to rollback optimistic updates on error.Callbacks:
onSuccessandonError.
Trigger Function Types: Overloaded signatures for trigger function depending on whether extra arguments are required.
SWRMutationResponse: The return type of the mutation hook, exposing mutation state and functions.
These types enable robust usage patterns and extensibility by developers leveraging the mutation hook.
Interaction with Other System Parts
Integration with Core SWR Cache (
useSWRConfig)
The mutation hook uses the global SWR cache andmutatefunction from the SWR configuration context to update cached data atomically.Middleware Architecture
The mutation feature is implemented as middleware wrapping the coreuseSWRhook viawithMiddleware, allowing it to extend core data fetching capabilities without duplicating logic.State Utilities
The module relies onuseStateWithDepsfor efficient state management and React’sstartTransitionfor smooth UI updates.Cache Serialization and Utilities
Utilities likeserializeandmergeObjectshelp handle keys and options merging consistently.External Usage
Components and other hooks can importuseSWRMutationto perform mutations related to cached data, enabling patterns like optimistic UI updates, manual mutation triggers, and error recovery.
Unique Design Patterns and Approaches
Middleware Composition
Mutation logic is composed as middleware layered on top of the core SWR hook, promoting modularity and reusability.Timestamp-Based Mutation Discarding
By tracking mutation start timestamps and ignoring outdated mutation results, the module handles concurrent mutation calls gracefully, preventing stale updates.Dependency-Tracked State
Fine-grained dependency tracking for mutation state ensures minimal re-renders, improving performance in complex UIs.Trigger Function Overloads
Thetriggerfunction supports multiple call signatures to accommodate mutations with or without extra arguments, and with flexible error handling (throwOnError).Optimistic Data and Rollback Configurations
Allows specifying optimistic updates and rollback conditions declaratively, simplifying UI consistency management.
Code Illustrations
Mutation Hook Initialization and State
const [stateRef, stateDependencies, setState] = useStateWithDeps({
data: UNDEFINED,
error: UNDEFINED,
isMutating: false
})
const currentState = stateRef.current
This sets up mutation state with dependency tracking for data, error, and isMutating.
Triggering a Mutation with Timestamp Guard
const mutationStartedAt = getTimestamp()
ditchMutationsUntilRef.current = mutationStartedAt
setState({ isMutating: true })
try {
const data = await mutate<Data>(
serializedKey,
fetcher(resolvedKey, { arg }),
{ ...options, throwOnError: true }
)
if (ditchMutationsUntilRef.current <= mutationStartedAt) {
startTransition(() => setState({ data, isMutating: false, error: undefined }))
options.onSuccess?.(data, serializedKey, options)
}
return data
} catch (error) {
if (ditchMutationsUntilRef.current <= mutationStartedAt) {
startTransition(() => setState({ error, isMutating: false }))
options.onError?.(error, serializedKey, options)
if (options.throwOnError) throw error
}
}
This snippet highlights the core mutation flow: marking mutation start time, triggering the fetcher, updating state conditionally based on the mutation timestamp, and invoking callbacks.
Resetting Mutation State
const reset = useCallback(() => {
ditchMutationsUntilRef.current = getTimestamp()
setState({ data: UNDEFINED, error: UNDEFINED, isMutating: false })
}, [])
Allows clearing mutation state and preventing stale mutation results from affecting the UI.
Mermaid Sequence Diagram: Remote Data Mutation Workflow
sequenceDiagram
participant Comp as React Component
participant Mutation as useSWRMutation Hook
participant SWRCache as SWR Cache
participant Fetcher as Mutation Fetcher
Comp->>Mutation: Calls trigger(arg, options)
Mutation->>Mutation: Set isMutating = true
Mutation->>SWRCache: mutate(key, fetcher(resolvedKey, { arg }))
SWRCache->>Fetcher: Execute fetcher request
Fetcher-->>SWRCache: Return mutation result or throw error
alt Mutation Success
SWRCache-->>Mutation: mutation data
Mutation->>Mutation: Set data, clear error, isMutating = false
Mutation->>Comp: Return data
else Mutation Failure
SWRCache-->>Mutation: error thrown
Mutation->>Mutation: Set error, clear data, isMutating = false
Mutation->>Comp: Throw or return error based on options
end
Comp->>Mutation: Optionally call reset()
Mutation->>Mutation: Clear mutation state
This diagram visualizes the interaction between a React component, the mutation hook, SWR cache, and the fetcher during a mutation lifecycle.
Summary
The Remote Data Mutation module is a focused extension of the SWR ecosystem that empowers developers to perform controlled remote data mutations with sophisticated state management, optimistic updates, rollback capability, and tight cache integration. Its design leverages middleware composition, timestamp-based concurrency control, and dependency-tracked state updates to provide a robust and flexible mutation API suitable for complex data-driven applications.