Local State Sharing

Purpose and Scope

The local state sharing example demonstrates a useful extension of SWR’s normal data-fetching model: a cache key can also represent client-owned state that multiple React components read and update. The official example states the intent directly: show how to share local state between React components using SWR. In practice, this means treating SWR’s cache as a small shared store for values that still benefit from the same subscription, rerender, and mutation machinery used for remote data. The pattern is most appropriate when a value should be visible to multiple components without introducing a separate state library.

Sources: README.md, src/_internal/utils/global-state.ts, src/mutation/state.ts

SWR’s README frames the library as a React Hooks data-fetching library built around cache, request deduplication, local mutation, and a stream of updates that keeps UI reactive. Those same terms matter for local state sharing. A component subscribes to a key, receives the current cached value, and rerenders when that value changes. Instead of thinking about the key as only a URL, think of it as a stable identifier for a shared resource. For local state, that resource might be a counter, selected item, theme preference, draft object, or another client-side value.

Sources: README.md

Relevant Source Files

  • README.md — Defines SWR’s public mental model: hooks, keys, cache, local mutation, and automatic streams of data updates.
  • e2e/site/README.md — Shows the repository’s local Next.js development workflow, which is useful when adapting or validating examples in an app.
  • src/_internal/utils/global-state.ts — Defines the internal WeakMap that associates each cache provider with SWR global state used for deduplication and listeners.
  • src/mutation/state.ts — Implements dependency-tracked state used by mutation hooks so updates rerender only components that read changed fields.

Core Primitives

The primary primitive is the SWR key. In the README quick start, the key is described as the unique identifier of a request, commonly an API URL, and the fetcher receives that key to load data asynchronously. For local state sharing, keep the uniqueness rule but relax the “request” assumption: the key can identify local cached state. A key such as local:sidebar-open or local:selected-project can be read by every component that needs the value. The important rule is that all readers and writers agree on the exact same key.

Sources: README.md

The second primitive is the cache. The README lists built-in cache and request deduplication as core capabilities, and the internal global state file shows that SWR stores global state in a WeakMap<Cache, GlobalState>. That design means SWR’s listener and deduplication bookkeeping is associated with a cache provider object. For a local-state recipe, this matters because a shared cache scope is what lets separate components observe the same key. If components are rendered under different cache providers, they can intentionally have isolated local state even if they use identical key strings.

Sources: README.md, src/_internal/utils/global-state.ts

The third primitive is mutation. The README names local mutation and optimistic UI as part of SWR’s feature set, and the mutation state implementation shows how SWR updates hook state carefully. useStateWithDeps keeps a mutable state reference, tracks whether render code accessed fields such as data, error, or isValidating, and rerenders only when an accessed field changes. For local state sharing, that means cache writes do not have to become broad, unconditional component rerenders. Consumers that read shared data are the ones that need to reflect the change.

Sources: README.md, src/mutation/state.ts

Example Workflow

Start the official example the same way as the repository’s example README describes: download the examples/local-state-sharing directory from the main branch archive, enter the extracted directory, install dependencies, and run the development server with either Yarn or npm. The e2e Next.js site README uses the same general local-app workflow: run the dev command, open the local browser URL, and edit files while the page auto-updates. That makes the local-state-sharing example easy to inspect interactively because you can change one component and immediately verify whether another component observes the same cached state.

Sources: e2e/site/README.md

curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/local-state-sharing
cd local-state-sharing
yarn
yarn dev
# or
npm install
npm run dev

A minimal local-state sharing pattern has three steps. First, choose a stable cache key that names the local value rather than a server endpoint. Second, read that key with useSWR in every component that needs the shared value. Third, update the value through SWR mutation so all subscribers see the same cache entry change. The fetcher can be unnecessary when the value is fully local and seeded from fallback or an initial mutation, but the README’s key-and-fetcher explanation is still the conceptual anchor: useSWR binds a component to the key and returns data, error, and loading state derived from that binding.

Sources: README.md

import useSWR, { mutate } from 'swr'
 
const key = 'local:counter'
 
function CounterValue() {
  const { data = 0 } = useSWR(key, null)
  return <p>Count: {data}</p>
}
 
function CounterButton() {
  const { data = 0 } = useSWR(key, null)
  return <button onClick={() => mutate(key, data + 1, false)}>Increment</button>
}

In this sketch, CounterValue and CounterButton do not pass props to each other and do not need a React context dedicated to the counter. They coordinate through SWR’s cache key. The final false argument in the mutation call represents the common local-state choice of not revalidating against a remote source after writing a value. If your local value is actually a projection of remote data, prefer a mutation flow that revalidates after the optimistic write. If the value is purely client-owned, skipping remote revalidation keeps the operation local and predictable.

Sources: README.md, src/mutation/state.ts

System-to-Code Mapping

At the system level, the recipe uses SWR as a small publish-subscribe cache. A reader subscribes by rendering a hook for a key. A writer updates the cache entry for that key. SWR’s global state then knows which cache provider owns the bookkeeping for listeners and request deduplication. The SWRGlobalState declaration is intentionally small, but it communicates an important boundary: global SWR runtime state is not a single process-wide object keyed only by strings; it is scoped by the cache provider object held in the WeakMap.

Sources: src/_internal/utils/global-state.ts

The mutation state implementation explains why the pattern scales beyond toy examples. useStateWithDeps stores the hook state in a ref, keeps a dependency map for fields read during render, and only schedules a React rerender when a changed field was actually read. It also guards against rerendering after unmount by tracking unmountedRef with an isomorphic layout effect. Local state sharing can create frequent updates, so this dependency-aware behavior is part of what keeps SWR suitable for reactive UI state rather than only occasional network responses.

Sources: src/mutation/state.ts

React version compatibility is also visible in the mutation state source. SWR exports a startTransition helper that directly invokes the callback for legacy React and delegates to React.startTransition otherwise. For local shared state, that means mutation-driven updates can participate in concurrent React scheduling when the environment supports it, while still working in older React configurations. The recipe author does not have to branch on React versions in component code; SWR hides that compatibility concern inside the mutation implementation.

Sources: src/mutation/state.ts

Practical Guidance and Tradeoffs

Use this pattern when the state is small, cache-addressable, and naturally shared across components that already use SWR. Examples include UI selections, lightweight form drafts, tab-local preferences, and derived client state that should be invalidated or reset by key. Avoid turning every piece of component state into an SWR key. If state is private to one component, useState is simpler. If state requires complex reducers, transactions, or persistence rules, a dedicated state manager may be clearer. SWR’s advantage is strongest when the same key also connects local updates, optimistic UI, and remote revalidation.

Sources: README.md

Be deliberate about cache provider scope. Because SWR’s global state is attached to the cache provider, a top-level provider can make local state visible across a whole application, while a nested provider can isolate state for a page, test, or embedded widget. That isolation is a feature when building examples: it prevents one demo area from leaking values into another. It is also a debugging clue. If two components use the same key but do not observe each other’s updates, check whether they are under the same SWR cache provider scope.

Sources: src/_internal/utils/global-state.ts

When adapting the example, keep the reader-facing flow close to the official example: run the app, identify two components that need a shared value, give the value a stable key, and replace prop drilling with SWR reads and mutations. Then test the interaction by updating from one component and confirming the other reflects the change. From here, read the mutation concepts page if the state needs optimistic remote persistence, the cache and provider page if you need scoped stores, and the global configuration page if the shared state should be initialized through fallback configuration.

Sources: README.md, e2e/site/README.md