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


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:

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
)

Mutation Workflow

  1. Triggering a Mutation
    Calling trigger executes 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.

  2. 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.

  3. State Updates During Mutation

    • Before mutation: sets isMutating to true.

    • On success: updates data, clears error, sets isMutating to false, and optionally calls onSuccess callback.

    • On failure: updates error, clears data, sets isMutating to false, and optionally calls onError callback.

  4. Cache Mutation
    The mutation internally calls SWR’s mutate method to update the global cache associated with the key, allowing cache synchronization and revalidation.

  5. Resetting State
    The reset method 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:

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:

These types enable robust usage patterns and extensibility by developers leveraging the mutation hook.


Interaction with Other System Parts


Unique Design Patterns and Approaches


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.