In SWR, how do you use mutate to show a user's new display name in the header immediately and roll it back if saving fails?
answer
- bound or global, same key
- write the guess first
- the save promise as data
- rollback is on by default
- then revalidate
basics
~20 sCall mutate for '/api/user' with the save promise and optimisticData set to the edited user. The header updates at once; if the promise rejects, rollbackOnError (default true) restores the previous data, and revalidate (default true) refetches afterwards.
solid answer
~50 sGet `mutate` either from the `useSWR('/api/user')` call (bound, key already fixed) or from `useSWRConfig()` (global, takes the key). Pass the async save as the `data` argument, plus options. `optimisticData` is written to the cache immediately, so every hook on `'/api/user'`, including the header, re-renders with the new name. When the promise resolves, `populateCache` (default `true`) writes its result into the cache, so the endpoint should return the full user, or you pass a function that merges the result into the current data. `revalidate` (default `true`) then refetches the key. If the promise rejects, `rollbackOnError` (default `true`) restores the data committed before the mutation, and `throwOnError` (default `true`) rethrows so the form can show the error. While the mutation is in flight, SWR discards revalidation results that overlap it, so a focus refetch cannot overwrite the optimistic name with old data.
code
tsx · 48 linesimport useSWR, { useSWRConfig } from 'swr'
import { useState } from 'react'
type User = { id: string; name: string }
async function saveName(name: string): Promise<User> {
const res = await fetch('/api/user', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }),
})
if (!res.ok) throw new Error('Could not save your name')
return res.json() // the full updated user
}
export function NameForm() {
const { data: user } = useSWR<User>('/api/user')
const { mutate } = useSWRConfig()
const [error, setError] = useState<string | null>(null)
async function onSubmit(name: string) {
if (!user) return
setError(null)
try {
await mutate('/api/user', saveName(name), {
optimisticData: { ...user, name },
rollbackOnError: true, // the default, written out for clarity
populateCache: true, // write the returned user into the cache
revalidate: true, // then refetch to be sure
})
} catch (e) {
setError(e instanceof Error ? e.message : 'Save failed')
}
}
return (
<form
onSubmit={(e) => {
e.preventDefault()
void onSubmit(String(new FormData(e.currentTarget).get('name') ?? ''))
}}
>
<input name="name" defaultValue={user?.name} />
<button type="submit">Save</button>
{error ? <p role="alert">{error}</p> : null}
</form>
)
}go deeper
Know that mutate updates the cached data for a key, and that every component using that key, like the header, re-renders with the new value.
Walk through optimisticData, populateCache, rollbackOnError and revalidate with their defaults, and explain the difference between bound and global mutate.
Handle the edge cases: endpoints that return partial data, validation errors that should not roll back, the global import missing a custom provider, and overlapping revalidations.
Decide which writes deserve optimistic updates at all, based on failure rates and how confusing a visible rollback is for users, and make that a documented pattern.
## Two ways to get mutate SWR's `mutate` writes to a cache entry and can revalidate it. It comes in two forms that do the same work: | Form | Where it comes from | Key | |---|---|---| | **Bound** | returned by `useSWR('/api/user', fetcher)` | fixed to that hook's key | | **Global** | `useSWRConfig().mutate`, or `import { mutate } from 'swr'` | passed as the first argument | Use the bound form inside the component that owns the data, and the global form when the code that saves is somewhere else, such as a settings form that never calls `useSWR('/api/user')` itself. The top-level import is bound to the **default** cache provider; under a custom `SWRConfig` provider, take `mutate` from `useSWRConfig()`. ## The optimistic sequence With `mutate(saveUser(newName), options)`: 1. SWR records that a mutation for `'/api/user'` has started. 2. If `optimisticData` is set, SWR writes it to the entry and keeps the previously **committed** data aside. Every subscriber, the header included, re-renders with the new name. 3. SWR awaits the promise you passed as `data`. 4. **On success**, `populateCache` decides what lands in the cache: the promise's result by default, or whatever your function returns. 5. **On failure**, if `rollbackOnError` allows it, SWR restores the committed data, and the header shows the old name again. With `throwOnError`, the error is rethrown to your code. 6. Finally, if `revalidate` is on, SWR refetches the key so the cache matches the server. ## The options and their defaults | Option | Default for `mutate` | What it does | |---|---|---| | `optimisticData` | not set | data, or a function of the current data, shown immediately | | `populateCache` | `true` | writes the resolved result to the cache; a function can merge it | | `revalidate` | `true` | refetches the key after the update | | `rollbackOnError` | `true` | restores the committed data if the promise rejects; can be a function of the error | | `throwOnError` | `true` | rethrows the error from `mutate` | ## Writing the result back - If the endpoint returns the **full updated user**, the default `populateCache: true` is exactly right. - If it returns **part** of the user, or something else entirely, pass `populateCache: (result, current) => ({ ...current, ...result })`, or set `populateCache: false` and rely on `revalidate`. - If the endpoint returns nothing useful, `populateCache: true` would write that value, typically `undefined`, into the entry until revalidation finishes. Set it to `false` in that case. ## Races SWR handles for you - A revalidation that **started before or during** the mutation has its result discarded, because it may describe the server before the save. With the default `revalidate: true`, the mutation's own revalidation then refreshes the entry. - Calling `mutate('/api/user')` with **only a key** writes nothing. It just revalidates, and only if a hook for that key is mounted. - `rollbackOnError` can be a **function** of the error, for example to keep the optimistic value when the failure is a validation error the form will display anyway. ## Optimistic data as a function `optimisticData` can also be a **function**. SWR calls it with the currently committed data and the data on screen, and uses its return value, so the update is computed from the latest cache instead of a snapshot captured when the handler was created: `optimisticData: (current) => ({ ...current, name })`. That matters when edits overlap. If a user saves twice quickly, the earlier mutation's result is **not written** once a later mutation has started for the same key. SWR checks the mutation timestamps and returns the earlier result to its caller without touching the cache. ## useSWRMutation for the same save `useSWRMutation('/api/user', updateUser)` from `'swr/mutation'` wraps the same machinery in a hook that only runs when you call **`trigger(arg)`**. It exposes `isMutating`, `data`, `error` and `reset`, and accepts the same options, with one difference to remember: **`populateCache` defaults to `false`** there. Its default `revalidate: true` still refetches `'/api/user'` afterwards, so the header ends up correct either way.
- Why pass the save as a promise to mutate instead of awaiting it first and then calling mutate with the result?Passing the promise lets SWR manage the whole sequence: show `optimisticData`, record the mutation so overlapping revalidations are discarded, roll back on rejection and revalidate at the end. Awaiting first and then calling `mutate(result)` gives you no optimistic update, no automatic rollback, and a window in which a revalidation can land old data.
- When would you choose useSWRMutation over calling mutate directly?When the component needs the mutation's own state, such as `isMutating` for a disabled button, `error` for a message and `reset`, or when the request needs arguments passed through `trigger(arg)`. Remember that its `populateCache` defaults to `false`, so the result is not written to the cache unless you enable it; its default revalidation still refetches the key.
saying these in an interview costs you the question
- Bound mutate only updates the component that called it.
- SWR does not roll back an optimistic update unless you write the rollback yourself.
- Calling mutate('/api/user') with only a key clears that key's data.
- populateCache defaults to false for mutate, so results never reach the cache.
- The global mutate imported from swr also reaches hooks under a custom cache provider.