skip to content

After one user logs out and another logs in, an SWR app briefly shows the previous user's profile and orders; why, and how do you clear SWR's cache correctly?

level: seniorimportance: should knowfreq 30%

answer

  1. the cache outlives the session
  2. the same keys for every user
  3. stale-while-revalidate, on the wrong data
  4. a clear-everything call since 2.5
  5. a provider per session

basics

~20 s

SWR's cache outlives the session, and '/api/user' is the same key for both users, so the new user first sees the old cached data. On logout call unload({ revalidate: false }) (SWR 2.5+), or give each session its own cache provider.

solid answer

~50 s

By default every hook shares one global cache, a `Map` that SWR never empties on its own. Logging out clears the cookie but not the cache, and user-scoped endpoints usually have user-agnostic keys. After the next login, `useSWR('/api/user')` finds an entry, returns it immediately with `isLoading` false, and replaces it only when revalidation finishes. That is stale-while-revalidate working as designed, on data that belongs to someone else. Requests from the old session that are still in flight can also land after logout. From SWR 2.5, call `unload({ revalidate: false })` on logout. It clears every entry in the provider, notifies mounted hooks, drops `keepPreviousData` snapshots and internal `useSWRInfinite` keys, and ignores in-flight results. The older recipe, `mutate(() => true, undefined, { revalidate: false })`, can miss those internal keys. Alternatively, render `<SWRConfig value={{ provider: () => new Map() }}>` keyed by the user id, so each session starts empty.

code

tsx · 22 lines
tsx
import { SWRConfig, useSWRConfig } from 'swr'
import type { ReactNode } from 'react'

// Option A: clear the cache on logout (SWR 2.5+).
export function useLogout(navigateToSignIn: () => void) {
  const { unload } = useSWRConfig() // scoped to the nearest provider

  return async function logout() {
    navigateToSignIn() // stop rendering authenticated hooks first
    await fetch('/api/logout', { method: 'POST' })
    unload({ revalidate: false }) // empty every entry, ignore in-flight results
  }
}

// Option B: give each signed-in user a fresh cache provider.
export function SessionCache({ userId, children }: { userId: string | null; children: ReactNode }) {
  return (
    <SWRConfig key={userId ?? 'anonymous'} value={{ provider: () => new Map() }}>
      {children}
    </SWRConfig>
  )
}

go deeper

for a junior

Know that SWR's cache is shared across the whole app and does not reset when a user logs out, so logout code has to clear it.

for a middle

Explain why identical keys plus stale-while-revalidate show the old user's data first, and name unload() and the mutate filter recipe for clearing the cache.

for a senior

Order the logout steps so nothing refetches with a dead session, use the provider-scoped functions under a custom SWRConfig, and account for in-flight requests landing late.

for a principal

Treat client caches as part of the session boundary in security reviews, especially on shared devices, and require a tested cache-clear path for every sign-out flow.

## Where SWR keeps data Unless you configure otherwise, every `useSWR` hook reads and writes one **global cache provider**, which is a plain `Map` created when the library loads. SWR has **no automatic eviction**. An entry stays until something removes it, even after every component using it has unmounted. That is useful for navigation, because returning to a page shows its data instantly. It also means the cache lives as long as the page does, across logins. ## Why the previous user appears 1. User A signs in and visits their profile and orders. The entries `'/api/user'` and `'/api/orders'` fill with A's data. 2. A logs out. The app clears the session cookie and redirects, but nothing touches SWR's cache. 3. User B signs in on the same tab. Their profile page calls `useSWR('/api/user')`, **the same key**, because the endpoint identifies the user from the cookie, not from the URL. 4. SWR finds the entry, returns **A's data immediately** with `isLoading` false, and starts a revalidation. 5. When B's response arrives, the entry updates. For that gap, B saw A's name and orders. A second path makes it worse. A request that A's session started just before logout can **resolve after** it and write A's data into the cache. ## Why revalidation alone is not enough It is tempting to argue that the next revalidation fixes everything. It does not: - The gap lasts a **full network round trip**, which on a slow connection is seconds of someone else's data on screen. - Any hook with automatic revalidation switched off, such as one using `useSWRImmutable` or `revalidateIfStale: false`, **never** replaces the old entry on its own. - `keepPreviousData` can hold the previous key's data on screen while a new key loads, so a list can briefly show the old user's results even under a new key. - The data is already in memory and visible to anything that reads the cache, whether or not it is ever rendered. ## Clearing the cache | Approach | What it clears | Notes | |---|---|---| | `unload()` or `unload({ revalidate: false })` | every entry in the provider, `keepPreviousData` snapshots and internal `useSWRInfinite` keys; results of in-flight requests are ignored | needs SWR 2.5.0-beta.1 or later; network requests are not aborted | | `mutate(() => true, undefined, { revalidate: false })` | every key the filter function receives | the older recipe; can miss internal keys such as those used by `useSWRInfinite` | | an `SWRConfig` provider keyed per session | everything, because the subtree gets a new `Map` | the whole subtree remounts on login | | keys that include the user id | nothing, but entries no longer collide | the old user's data stays in memory | `unload()` **revalidates** mounted hooks by default. On logout that would refetch user-scoped keys with no valid session, so pass `{ revalidate: false }`. Hooks then re-render with `data` undefined and stay empty until something revalidates them. ## Order of operations on logout 1. Stop rendering authenticated screens, for example by navigating to a signed-out route, so no hook immediately asks for user data again. 2. End the session on the server and clear any client-side token. 3. Call `unload({ revalidate: false })`, or the scoped version for your provider. 4. Show the sign-in page. The next user's hooks start from an empty cache, with `isLoading` true, instead of from stale data. ## Custom providers and the top-level imports - `import { unload, mutate } from 'swr'` are bound to the **default** provider. Under `<SWRConfig value={{ provider }}>` they do not reach your hooks; the docs warn that the global `mutate` will not work there. - Inside a custom provider, take the scoped functions from **`useSWRConfig()`**: `const { unload, mutate } = useSWRConfig()`. - A provider function in `SWRConfig` runs again when that `SWRConfig` remounts. Keying it by user id (`<SWRConfig key={userId} …>`) gives each session a fresh `Map`, at the cost of remounting everything below it. ## The same tool in tests The docs recommend wrapping each test render in `<SWRConfig value={{ provider: () => new Map() }}>`. It is the same isolation idea: each test, like each session, starts with an empty cache, so data from one never shows up in another.

  • Why not simply include the user id in every key instead of clearing the cache?
    It stops the new user from seeing the old user's entries, because the keys no longer collide, but the previous user's data stays in memory for the life of the page. Every hook also needs the id before it can fetch. It works well as a second layer, but on a shared device you still want the data gone at logout.
  • Does unload() cancel the requests that are still running?
    No. The docs say the underlying network operations are not aborted. SWR ignores their results when they resolve, so they cannot write the old user's data back into the cleared cache. The server still does the work.

saying these in an interview costs you the question

  • Clearing the auth cookie also clears SWR's cache.
  • SWR evicts entries automatically once no component uses them.
  • The global mutate imported from swr reaches hooks under any provider.
  • unload() without options is safe on logout because it never refetches.
  • Revalidation on mount makes stale data from another user harmless.