skip to content

In Jotai 3, what happens when a component reads an async atom with useAtomValue, and how do you avoid suspending or cancel a stale request?

level: seniorimportance: should knowfreq 35%

answer

  1. the value is a promise
  2. a boundary above the reader
  3. dependents inherit the promise
  4. a sync view with a fallback
  5. the second argument to read

basics

~20 s

An async atom's value is a promise, so useAtomValue suspends the reader until it settles; a Suspense boundary must sit above it and errors reach an error boundary. unwrap gives a non-suspending view, and the read's signal aborts stale fetches.

solid answer

~40 s

An atom whose read function is `async` holds a **promise** as its value. `useAtomValue` resolves it through React's `use()` (with a shim on React 18), so the component **suspends** until the promise settles: you need a `<Suspense>` boundary above it, inside any Jotai `Provider`, and a rejection goes to an error boundary. Atoms that read it with `get` receive the promise too, so they must be async themselves (`await get(asyncAtom)`) or read an `unwrap`ped version. To avoid suspending, `unwrap(asyncAtom, (prev) => prev ?? fallback)` from `jotai/utils` yields a synchronous atom that shows the fallback, or the previous value, while pending; Jotai 3 removed `loadable` in its favour. For cancellation, the read function's second argument carries a `signal`; pass it to `fetch`, and Jotai aborts it when a dependency change starts a new computation.

code

tsx · 25 lines
tsx
import { Suspense } from 'react'
import { useAtomValue } from 'jotai'
import { unwrap } from 'jotai/utils'
import { usernameAvailableAtom } from './username-atoms'

// synchronous view: keeps the last answer while a new check is pending
const availableNowAtom = unwrap(usernameAvailableAtom, (prev) => prev ?? false)

export function AvailabilityHint() {
  const available = useAtomValue(availableNowAtom) // never suspends
  return <small>{available ? 'available' : 'taken or too short'}</small>
}

function AvailabilityStrict() {
  const available = useAtomValue(usernameAvailableAtom) // suspends while pending
  return <small>{available ? 'available' : 'taken'}</small>
}

export function StrictHint() {
  return (
    <Suspense fallback={<small>checking...</small>}>
      <AvailabilityStrict />
    </Suspense>
  )
}

go deeper

for a junior

Know that an async atom makes its reader suspend, so a Suspense boundary is required above the component, and errors go to an error boundary.

for a middle

Explain why get returns a promise inside read functions, when a derived atom must become async, and how unwrap offers a synchronous view with a fallback.

for a senior

Prevent fallback flashes and request races in real forms: unwrap with the previous value, pass the read signal to fetch, and place Suspense inside the Provider.

for a principal

Decide where async data belongs: short-lived derived checks can live in atoms, while cached server data with retries and invalidation may belong to a dedicated data library.

## Async atoms hold promises In Jotai a read function may return a promise. The usual way to write one is an `async` read function, and the form scenario has a natural case: checking whether a username is available as the user types. ```ts import { atom } from 'jotai' export const usernameAtom = atom('') export const usernameAvailableAtom = atom(async (get, { signal }) => { const name = get(usernameAtom) if (name.length < 3) return false const res = await fetch(`/api/username-available?name=${encodeURIComponent(name)}`, { signal }) return ((await res.json()) as { available: boolean }).available }) ``` The store keeps the **promise** as `usernameAvailableAtom`'s value, and a new promise replaces it whenever `usernameAtom` changes. ## What `useAtomValue` does with it In Jotai 3, `useAtomValue` is `useAtomValueRaw` plus React's `use()`: it reads the raw value from the store and, if that value is a promise, resolves it through `use()` (on React 18, which has no `use()`, Jotai falls back to throwing the promise itself). The practical consequences: 1. The component **suspends** while the promise is pending, and the nearest `<Suspense>` boundary shows its fallback. 2. When the promise resolves, the component renders with the resolved value; `useAtom` behaves the same for the value it returns. 3. When it rejects, the error is thrown during render and reaches the nearest **error boundary**. The Jotai docs add a placement rule: if you use a `<Provider>`, put at least one `<Suspense>` **inside** it, otherwise rendering can loop endlessly. ## Deriving from an async atom `get` inside a read function does not resolve promises; `get(usernameAvailableAtom)` returns the promise. A derived atom therefore has two options: - **Become async itself**: `atom(async (get) => (await get(usernameAvailableAtom)) && get(emailIsValidAtom))`. Anything reading it suspends too. - **Read an unwrapped view**: derive from `unwrap(usernameAvailableAtom, () => false)`, which is synchronous, so the form's `isValidAtom` stays synchronous and the submit button never suspends. ## Not suspending: `unwrap` and the raw hooks | Tool | Where it lives | While pending | On error | |---|---|---|---| | `unwrap(a)` | `jotai/utils` | `undefined` | rethrows when read | | `unwrap(a, (prev) => prev ?? false)` | `jotai/utils` | fallback, or the previous resolved value | rethrows when read | | `useAtomValueRaw(a)` | `jotai` | the promise itself, no suspension | the promise rejects | | `loadable(a)` | removed in Jotai 3 | — | — | `unwrap` turns an async atom into a synchronous one: the fallback function receives the previous resolved value, so passing `(prev) => prev ?? false` keeps the last answer on screen while a new check runs instead of flashing a spinner. Jotai 3 removed `loadable`; the migration guide shows how to rebuild its `{ state: 'loading' | 'hasData' | 'hasError' }` shape in userland on top of `unwrap`. `useAtomValueRaw` is a lower-level v3 hook for components that want to handle the promise themselves. ## Cancelling stale requests The read function's second parameter is an options object with a `signal`, an `AbortSignal` created lazily on first access. When a dependency changes and the store starts a new computation, the previous computation's signal is aborted. Passing it to `fetch`, as above, means that typing `a`, `al`, `ali` quickly does not leave three requests racing: the older ones are aborted, and only the latest promise is stored. For work other than `fetch`, check `signal.aborted` or listen for its `abort` event. ## Async writes A write function may also be `async`, for example a submit action that posts the form and then calls `set` with the server's response. Writing does not suspend anything by itself; components suspend only when they read an atom whose value is a pending promise. ## Checklist - A component reading an async atom freezes on the fallback forever: check that the promise can settle and the `Suspense` is placed inside the `Provider`. - A whole form section disappears into a fallback on each keystroke: the synchronous `isValidAtom` became async by reading an async atom; derive from an `unwrap`ped view instead. - Results arrive out of order: pass `signal` to the request.

  • In Jotai, why does a derived atom that calls get(asyncAtom) and compares it to true always return false?
    `get` in a read function returns the async atom's value, which is a promise, not the resolved boolean. Comparing a promise to `true` is always false. Make the derived atom async and `await get(asyncAtom)`, or read an `unwrap`ped version of the async atom so the derived atom stays synchronous.
  • What replaces Jotai 2's loadable util after moving to Jotai 3?
    Jotai 3 removed `loadable`. Use `unwrap` from `jotai/utils` with a fallback for the pending state, or rebuild `loadable` in userland: the migration guide shows a small function that wraps `unwrap` and returns `{ state: 'loading' }`, `{ state: 'hasData', data }` or `{ state: 'hasError', error }`.
  • When does Jotai abort the signal passed to an async read function?
    When the atom is recomputed because a dependency changed, the store replaces the pending promise with a new one and aborts the previous computation's signal. The docs describe it as abort being triggered before the new calculation starts. Code that ignores the signal still runs to completion; only its result is no longer the atom's value.

saying these in an interview costs you the question

  • useAtomValue returns undefined while an async atom is loading.
  • get(asyncAtom) inside a read function returns the resolved value.
  • In Jotai 3, loadable is still the way to read an async atom without suspending.
  • Jotai cancels in-flight fetches automatically even if the signal is not passed on.
  • A Suspense boundary outside the Jotai Provider is always sufficient.