Storage Tab Sync
Purpose and Scope
The Storage Tab Sync example demonstrates a small but useful pattern: treating SWR as the React-facing state layer for values that are also persisted in browser storage. The example’s stated idea is to show how localStorage values can be synchronized between tabs. In practice, this means the UI should not think of localStorage as an isolated imperative API. Instead, components can read through an SWR key, render from SWR state, and update that state when another tab changes the same stored value.
Sources: README.md
This page focuses on the example workflow and the SWR concepts needed to understand it. SWR is described by the repository as a React Hooks library for data fetching, but the same primitives also fit local browser data because SWR is transport and protocol agnostic. The important part is that a key identifies the resource, a fetcher or reader resolves the current value, and SWR supplies reactive render state to components. For tab synchronization, the “resource” is not an HTTP endpoint; it is a named localStorage value shared by same-origin browser tabs.
Sources: README.md
Relevant Source Files
README.md- Defines SWR as a React Hooks data-fetching library, explains the stale-while-revalidate model, introducesuseSWR(key, fetcher), and lists built-in cache, real-time experience, revalidation, polling, local mutation, and reactive data updates as core capabilities.e2e/site/README.md- Shows the standard Next.js development-server workflow used by repository example and test applications: install dependencies, run the development server, and open the local app in a browser.
Core Primitives
The first primitive is the SWR key. The README describes the key as a unique identifier of the request, normally the URL of an API. In the storage-tab-sync pattern, the key should instead identify the localStorage entry being mirrored into React state. That translation is the central design decision: once a storage item has a stable SWR key, every component that calls the same hook can subscribe to the same cached state and receive updates through the normal SWR rendering path.
Sources: README.md
The second primitive is the fetcher. In the README quick start, the fetcher accepts the key and returns data asynchronously, and SWR does not require a particular transport library. For this example, the fetcher can be understood as a storage reader rather than a network client. It can read the current value from localStorage, parse it if needed, and return the value in the shape the component expects. This keeps React components declarative: they ask SWR for the value and render from data, rather than manually reading storage during every render.
Sources: README.md
The third primitive is SWR’s returned state. The README names data, isLoading, and error as the main values in the quick-start example. The same shape gives the storage example a predictable UI contract. A component can render an initial loading or empty state while the browser value is read, display an error if parsing or access fails, and then render the synchronized value once data is available. Even when the backing data is local, the component still benefits from the same hook interface used for remote API data.
Sources: README.md
Running the Example
The official example workflow is the same lightweight flow used across SWR examples: download only the example directory, install dependencies, and run the development server. The example can also be deployed with Vercel using the one-click deploy link from the official example page. Running it locally is especially helpful for this pattern because the behavior only becomes clear when the same origin is open in more than one browser tab.
curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/storage-tab-sync
cd storage-tab-sync
yarn
yarn dev
# or
npm install
npm run devThe E2E site README gives the general local-app expectation for repository applications: run a development server and open the localhost URL in a browser. For this storage example, open the app in two tabs after the dev server starts. Change the stored value in one tab, then observe the other tab. The point of the example is not just persistence; it is cross-tab synchronization, so testing with a single tab only verifies the easiest part of the pattern.
Sources: e2e/site/README.md
System-to-Code Mapping
At the system level, this example has three roles. The browser owns localStorage and makes it available to tabs from the same origin. SWR owns the React-facing cache entry and notifies hook consumers when that entry changes. The example UI owns user interaction and renders the current value. Keeping those roles separate prevents the common mistake of mixing storage reads, event listeners, and rendering logic directly inside many unrelated components.
Sources: README.md
The mapping starts with the SWR key. Choose one key for the stored value and reuse it wherever the value is displayed or edited. The fetcher reads from localStorage for that key and returns the current value. When the user changes the value, the application should write the new value to localStorage and update the corresponding SWR cache entry so the current tab rerenders immediately. When another tab changes the same localStorage value, the application should propagate that external change back into the same SWR key so subscribers in the current tab see the new value.
Sources: README.md
This pattern lines up with the README’s description of SWR as cache-first and reactive. SWR first returns cached data when available, then updates components as fresh data arrives. With localStorage, “fresh” does not necessarily mean a network response. It can mean a newly read browser value or a value received through a cross-tab notification path. The implementation should preserve the same user experience goal: the UI stays fast because it renders from cache, then remains correct because cache changes are propagated consistently.
Sources: README.md
Execution Flow
A practical flow starts when the page mounts. The component calls useSWR with the storage key and a fetcher that reads localStorage. If SWR already has a cached value, the component can render it immediately. If not, the component waits for the fetcher and uses the loading state the same way it would for an API request. This keeps the rendering path familiar for developers who have already used the README quick-start pattern.
Sources: README.md
Next, the user edits the value in one tab. The local tab should update both the durable storage value and the SWR cache value. Updating the cache gives immediate feedback and avoids forcing the UI to wait for a later re-read. The README highlights local mutation and a real-time experience among SWR’s capabilities; this example is a compact illustration of that idea with browser storage instead of a remote backend.
Sources: README.md
Finally, another tab must observe that storage changed and update its own SWR cache. The example’s purpose is to synchronize localStorage values between tabs, so the important verification step is watching whether the second tab’s React state changes after the first tab writes the value. If it does, components in both tabs are rendering from a shared conceptual source of truth even though each tab has its own JavaScript runtime and SWR cache instance.
Sources: README.md
Implementation Notes and Next Steps
Use this example when you need lightweight shared browser state, such as a theme choice, draft setting, selected workspace, or other same-origin preference. It is not a replacement for server-backed synchronization when users need the same value across devices or accounts. The strongest fit is state that naturally belongs in localStorage but still needs React components to update predictably across tabs without ad hoc imperative reads scattered through the tree.
Sources: README.md
After exploring this example, compare it with the local state sharing and optimistic UI examples. Local state sharing explains how SWR can coordinate state between components in one tab, while storage tab sync extends the idea across browser tabs by involving localStorage. If the value is remote or user-specific, move next to mutation concepts and the mutate API so cache writes, optimistic updates, rollback, and revalidation are handled with the same SWR vocabulary rather than a separate state-management model.
Sources: README.md