Optimistic UI

Purpose and Scope

This page explains the Optimistic UI example pattern in SWR: update the interface immediately by mutating cached data, then let the API response and revalidation settle the final state. The pattern is useful when an application has a user action such as saving a profile, starring an item, adding a comment, or toggling a setting. Instead of waiting for a round trip before showing feedback, the component writes the expected result into SWR-managed state first, making the UI feel responsive while preserving the cache-and-revalidate model that SWR is built around.

Sources: README.md

SWR’s README frames the library as a React Hooks data-fetching library whose name comes from stale-while-revalidate. In that model, SWR first returns cached data, then sends a request, and finally provides the up-to-date result. Optimistic UI is a natural extension of that lifecycle: local cache data becomes the immediate source of truth for the interaction, and a later revalidation reconciles it with remote data. The README explicitly lists “Local mutation (Optimistic UI)” among the capabilities supported by the library, tying this example to a first-class SWR use case rather than to a one-off demo.

Sources: README.md

The official example description summarizes the recipe as using SWR to mutate cached data immediately and then trigger a revalidation with the API. In practice, that means the page does not teach a separate state-management system. It teaches how to let SWR’s cache represent both the currently known server value and the temporary optimistic value. Components that already read from SWR can then re-render from the same cache entry, so the optimistic state is shared wherever the key is used.

Relevant Source Files

  • README.md — Establishes SWR as a React Hooks data-fetching library, defines the stale-while-revalidate cache lifecycle, shows the basic useSWR(key, fetcher) shape, and names Local mutation / Optimistic UI as a supported capability.
  • e2e/site/README.md — Documents the repository’s Next.js E2E app workflow, including local development commands, browser entrypoint, and API route conventions that are useful when adapting an optimistic UI example to an app with API endpoints.

Core Primitives

The main primitive behind this example is the SWR cache entry identified by a key. The README’s quick start explains that a key is a unique identifier for a request, normally the API URL, and that the fetcher receives that key and returns data asynchronously. For optimistic UI, the key is still the coordination point. The component reads data from the key, the user action updates data associated with the same key, and revalidation asks the fetcher to refresh that key from the API.

Sources: README.md

The second primitive is SWR’s loading and error contract. The README shows that useSWR returns data, error, and isLoading, and that the component renders different UI depending on whether the fetcher has completed. An optimistic interaction usually happens after initial data exists, so the UI can present the current value, apply the expected local update, and still keep error handling available. If the remote request fails, the application should decide whether to roll back, show an error, or keep the optimistic value with a retry affordance.

Sources: README.md

The third primitive is revalidation. SWR’s central promise is not merely caching; it is returning cached data first and then updating that cached data when fresh data arrives. In an optimistic example, the local cache write is intentionally ahead of the server. Revalidation is the follow-up step that confirms the mutation, replaces the optimistic value with canonical data, or exposes failure. This sequencing is why the example is better understood as part of SWR’s data-flow model than as a visual trick.

Running the Example

The official optimistic UI example can be downloaded directly from the repository archive, installed, and run as a standalone project. These commands follow the same style as the repository’s example READMEs and are useful when you want to inspect the behavior in a browser before copying the pattern into an application.

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

The repository also includes a Next.js E2E site README that describes the standard development loop for a local Next.js app: run the development server, open http://localhost:3000, and edit pages or routes while the app updates. That matters for optimistic UI because the recipe usually spans both a React page and an API endpoint. The UI performs the immediate cache update, while an API route or server endpoint persists the change and provides the value used during revalidation.

Sources: e2e/site/README.md

When adapting the example to a Next.js project, keep the client and server responsibilities separate. The client should own the perceived latency improvement: update the SWR cache in response to a user action and render the new value right away. The API should own durability and validation: accept the mutation request, reject invalid changes, and return the canonical state. The E2E README notes that files in pages/api are mapped to /api/*, which is the kind of endpoint layout a small optimistic UI demo can use.

Sources: e2e/site/README.md

Implementation Flow

A typical optimistic flow begins with a component that already renders server data through useSWR. The initial render may show loading, error, or data states as described in the README quick start. Once data is present, the user performs an action. Instead of blocking the UI until the fetcher completes, the event handler computes the expected next state and writes it to the SWR cache for the same key. Any component reading that key can now show the optimistic value immediately.

Sources: README.md

After the local mutation, the application sends the actual request to the API. This is the moment where optimistic UI differs from purely local state: the local value is provisional, not final. The example’s stated goal is to trigger revalidation with the API after mutating cached data. Revalidation lets SWR return to its normal stale-while-revalidate contract, where cached data is visible first and fresh data replaces it later. The final UI should be derived from the refreshed cache entry, not from a separate local copy that can drift from SWR.

Sources: README.md

A compact version of the pattern looks like this. Treat it as a shape to adapt rather than as the full source of the example app:

import useSWR, { mutate } from 'swr'
 
function TodoButton() {
  const { data, error, isLoading } = useSWR('/api/todo', fetcher)
 
  async function toggle() {
    const optimistic = { ...data, completed: !data.completed }
    mutate('/api/todo', optimistic, false)
    await fetch('/api/todo', { method: 'POST', body: JSON.stringify(optimistic) })
    mutate('/api/todo')
  }
 
  if (error) return <p>failed to load</p>
  if (isLoading) return <p>loading...</p>
  return <button onClick={toggle}>{data.completed ? 'Done' : 'Todo'}</button>
}

The important parts are the order and the shared key. The local cache update comes first so the interface responds immediately. The remote request comes next so the server has a chance to accept or reject the change. The final revalidation comes last so the cache can converge on the API’s current data. This preserves SWR’s core property from the README: components receive a stream of data updates automatically, and the UI remains fast and reactive while still being corrected by fresh data.

Sources: README.md

Design Considerations

Optimistic UI is most effective when the expected result is easy to predict. Toggles, appends, deletions, and small edits are good candidates because the client can compute a plausible next value. More complex writes may require the server to assign IDs, normalize data, or enforce rules, so the optimistic value should be treated as temporary. The example’s revalidation step is what prevents the temporary value from becoming a long-lived fork of server state.

Another important design choice is error behavior. The README’s quick start models errors as part of the hook result, which means the UI should have a plan for failed revalidation or failed mutation requests. Some products roll back to the previous cached value, some leave the optimistic value visible with an error banner, and some disable the control while retrying. The right choice depends on whether incorrect temporary display is worse than a jarring rollback.

Sources: README.md

Finally, keep the example aligned with SWR’s transport-agnostic design. The README notes that the fetcher can be any asynchronous function and that developers can use their preferred data-fetching library. That means the optimistic UI pattern is not tied to fetch, REST, or a specific Next.js API route. What must stay consistent is the SWR key, the cache update, and the revalidation step that asks the chosen fetcher to retrieve the authoritative value.

Sources: README.md

Next Steps

Use this example when you already understand basic SWR fetching and want a faster interaction after a write. Start by identifying the SWR key that represents the resource, then implement the immediate cached update, the remote API request, and the final revalidation in that order. If your update logic becomes nested or difficult to express immutably, compare this recipe with the Immer variant, which applies the same optimistic idea with a different state-update style.

Related pages to read next: mutation-concepts for the broader mutation model, api-mutate for the mutation API surface, example-optimistic-ui-immer for the Immer-based recipe, and revalidation-strategies for the revalidation triggers that keep cached data fresh after local changes. Together, those pages explain how this example fits into SWR’s larger cache-first, revalidate-afterward architecture.