In Jotai, what are primitive, read-only derived and writable derived atoms, and which hook do you use to read or write each?
answer
- one function, several shapes
- a value versus a read function
- a second argument makes it writable
- tuple, value, or setter only
- the config holds no value
basics
~20 satom(initial) makes a primitive atom; atom(get => ...) a read-only derived atom; atom(read, write) a writable derived one, with atom(null, write) as a write-only action. useAtom returns [value, set], useAtomValue the value, useSetAtom only the setter.
solid answer
~40 sEverything in Jotai starts from `atom`. Passing a value, `atom('')`, creates a **primitive** atom you can read and set. Passing a read function, `atom((get) => get(emailAtom).includes('@'))`, creates a **read-only derived** atom whose dependencies are the atoms it calls `get` on. Passing a read function and a write function, `atom(read, (get, set, arg) => ...)`, creates a **writable derived** atom; `atom(null, write)` is the common write-only "action" atom. The atom is only a config object: values live in a store. In components, `useAtom(a)` returns `[value, setValue]` like `useState`, `useAtomValue(a)` returns just the value, and `useSetAtom(a)` returns just the setter without subscribing, so a component that only writes does not re-render when the value changes.
code
tsx · 17 linesimport { useAtom, useAtomValue, useSetAtom } from 'jotai'
import { emailAtom, isValidAtom, resetFormAtom } from './form-atoms'
export function EmailInput() {
const [email, setEmail] = useAtom(emailAtom)
return <input value={email} onChange={(e) => setEmail(e.target.value)} />
}
export function SubmitButton() {
const isValid = useAtomValue(isValidAtom)
return <button disabled={!isValid}>Sign up</button>
}
export function ResetLink() {
const reset = useSetAtom(resetFormAtom) // no subscription, no re-render
return <button type="button" onClick={() => reset()}>Clear</button>
}go deeper
Recall the call shapes: atom(value), atom(read), atom(read, write) and atom(null, write), and match each to useAtom, useAtomValue or useSetAtom.
Explain that get inside a read function records dependencies each time it runs, and why useSetAtom avoids re-renders by not subscribing at all.
Use hook choice and derived atoms to shape render scope deliberately, and put multi-atom updates in write-only action atoms rather than in components.
Agree team conventions on where atoms live, how action atoms are named, and when a derived atom is preferred over computing in a component.
## An atom is a config, not a container Jotai builds state bottom-up from **atoms**. Calling `atom(...)` returns a small **config object** that describes how to read and, optionally, how to write a piece of state. It does not hold a value. Values live in a **store**: the store behind the nearest `Provider`, or the default store when there is none. The same config can therefore have different values in different stores, and the config itself is safe to define once at module scope. ## The shapes `atom()` can create The `atom` function is overloaded; what you pass decides what you get: | Call | Kind | Readable | Writable | |---|---|---|---| | `atom('')` | primitive | yes, its current value | yes, a value or an updater `(prev) => next` | | `atom((get) => ...)` | read-only derived | yes, computed by the read function | no | | `atom((get) => ..., (get, set, arg) => ...)` | writable derived | yes, computed | yes, runs the write function | | `atom(null, (get, set, arg) => ...)` | write-only (action) | `null` | yes, runs the write function | The form scenario shows all of them together: ```ts import { atom } from 'jotai' export const emailAtom = atom('') export const passwordAtom = atom('') // read-only derived: recomputed when a field it reads changes export const isValidAtom = atom( (get) => get(emailAtom).includes('@') && get(passwordAtom).length >= 8, ) // write-only action: one call updates several atoms export const resetFormAtom = atom(null, (_get, set) => { set(emailAtom, '') set(passwordAtom, '') }) ``` A few details matter: - Inside a read function, `get(someAtom)` both returns that atom's value and records it as a **dependency**. Dependencies are refreshed every time the read function runs, so conditional `get` calls are tracked correctly. - Inside a write function, `get` reads without subscribing and `set(target, value)` writes any writable atom, including other derived ones. - A primitive atom's default write accepts either a value or an updater function, just like a `useState` setter. Because `atom(fn)` is read as a read function, storing a function as a value needs wrapping, for example `atom({ fn })`. ## The three hooks Components talk to atoms through hooks exported from `jotai`: 1. `useAtom(anAtom)` returns `[value, setValue]`. It subscribes to the value and gives you the write function; the argument you pass to `setValue` becomes the third parameter of the atom's write function. 2. `useAtomValue(anAtom)` returns only the value and subscribes to it. Use it for read-only derived atoms and anywhere you do not write. 3. `useSetAtom(anAtom)` returns only the setter and **does not subscribe**, so the component does not re-render when the atom's value changes. For the form, each input uses `useAtom(emailAtom)`, the submit button uses `useAtomValue(isValidAtom)` to decide whether it is disabled, and a reset link uses `useSetAtom(resetFormAtom)`. ## Why the split between hooks matters `useAtom` is simply the pair of the other two. Choosing the narrower hook is how you control **render scope** in Jotai: - A component that only triggers an action should use `useSetAtom`; with `useAtom` it would re-render whenever the atom's value changed even though it never displays it. - A component that only displays should use `useAtomValue`; it expresses intent and avoids pulling a setter it never calls. - Derived atoms narrow scope further: the submit button re-renders when the **boolean** `isValid` flips, not on every keystroke in the email field. ## Where the values come from When a component first reads an atom in a store, the store initialises it: a primitive gets its initial value, a derived atom runs its read function. When no component uses an atom any more and the config is unreachable, the stored value can be garbage-collected, because the store keys values by the atom object. That identity-based keying is also why atoms must not be recreated on every render. ## Common mistakes - Expecting `emailAtom` itself to hold the string, and reading `emailAtom.value`. - Calling `useAtom` in a button that never shows the value, then chasing extra re-renders. - Trying to set a read-only derived atom; it has no write function, and TypeScript rejects it.
- In Jotai, how would you let a single setter update emailAtom and also trim the input?Wrap it in a writable derived atom: `atom((get) => get(emailAtom), (_get, set, next: string) => set(emailAtom, next.trim()))`. Components use this atom instead of the primitive; reading returns the stored email and writing runs your function. The argument passed to the setter arrives as the write function's third parameter.
- Can a Jotai write function update several atoms in one call?Yes. The write function receives `set` and may call it for any number of writable atoms, as `resetFormAtom` does for both fields. Components subscribed to the affected atoms, or to derived atoms reading them, are notified after the writes; a component calling it through `useSetAtom` does not subscribe to anything.
saying these in an interview costs you the question
- An atom object stores its own value, so you can read emailAtom.value directly.
- useSetAtom subscribes to the atom just like useAtom, it only hides the value.
- Derived atoms must list their dependencies in an array, like a hook's deps.
- A read-only derived atom can be overwritten by calling its setter.
- Passing a function to atom() stores that function as the initial value.