Atoms are the fundamental units of state in Jotai. An atom represents a piece of state that can be read and written. Jotai supports several types of atoms, from simple primitive atoms to complex derived atoms.
Primitive Atoms
A primitive atom is the simplest form of atom. It holds a single piece of state and can be both read and written.
import { atom } from 'jotai'
const countAtom = atom(0)
const nameAtom = atom('John')
const todosAtom = atom(['Buy milk', 'Walk dog'])
Type Signature
type PrimitiveAtom<Value> = WritableAtom<
Value,
[SetStateAction<Value>],
void
>
type SetStateAction<Value> = Value | ((prev: Value) => Value)
Primitive atoms accept either a direct value or a function that receives the previous value and returns the new value, similar to React.useState.
Atoms Without Initial Values
You can create atoms without initial values. These atoms will have undefined as their initial value:
const optionalAtom = atom<string>()
// Type: PrimitiveAtom<string | undefined>
Derived Atoms
Derived atoms compute their value based on other atoms. They automatically update when their dependencies change.
Read-Only Derived Atoms
Create a read-only atom by passing a read function:
const countAtom = atom(0)
const doubleCountAtom = atom((get) => get(countAtom) * 2)
The get function allows you to read the value of any atom:
type Getter = <Value>(atom: Atom<Value>) => Value
Combining Multiple Atoms
You can derive state from multiple atoms:
const firstNameAtom = atom('John')
const lastNameAtom = atom('Doe')
const fullNameAtom = atom((get) => {
const firstName = get(firstNameAtom)
const lastName = get(lastNameAtom)
return `${firstName} ${lastName}`
})
Functional Programming Patterns
For functional programming enthusiasts:
const count1 = atom(1)
const count2 = atom(2)
const count3 = atom(3)
const atoms = [count1, count2, count3]
const sumAtom = atom((get) =>
atoms.map(get).reduce((acc, count) => acc + count, 0)
)
Writable Derived Atoms
Create atoms that can both read and write to other atoms by providing both read and write functions:
const countAtom = atom(0)
const incrementAtom = atom(
(get) => get(countAtom),
(get, set) => set(countAtom, get(countAtom) + 1)
)
Type Signature
type Read<Value> = (
get: Getter,
options: { readonly signal: AbortSignal }
) => Value
type Write<Args extends unknown[], Result> = (
get: Getter,
set: Setter,
...args: Args
) => Result
type Setter = <Value, Args extends unknown[], Result>(
atom: WritableAtom<Value, Args, Result>,
...args: Args
) => Result
The write function receives:
get - Read any atom’s current value
set - Update any writable atom
...args - Custom arguments passed when calling the setter
Custom Arguments
Your write function can accept custom arguments:
const countAtom = atom(0)
const multiplyAtom = atom(
(get) => get(countAtom),
(get, set, multiplier: number) => {
set(countAtom, get(countAtom) * multiplier)
}
)
// Usage: setMultiply(5)
Write-Only Atoms
Create write-only atoms by passing null as the first argument:
const countAtom = atom(0)
const incrementByAtom = atom(
null,
(get, set, by: number) => set(countAtom, get(countAtom) + by)
)
Write-only atoms are useful for creating action-like atoms that don’t need to expose their own value.
When using write-only atoms with useAtom, the read value will be null, so you typically destructure as [, increment] = useAtom(incrementByAtom).
Atom Configuration
Debug Labels
Add debug labels to atoms for better debugging experience:
const countAtom = atom(0)
countAtom.debugLabel = 'count'
In development mode, the atom’s toString() method will return the key combined with the debug label (e.g., "atom1:count").
onMount
The onMount function is called when the atom is first subscribed to in the store:
const countAtom = atom(0)
countAtom.onMount = (setAtom) => {
console.log('atom mounted')
// Return cleanup function
return () => {
console.log('atom unmounted')
}
}
The onMount function receives setAtom which can be used to update the atom’s value:
const clockAtom = atom(new Date())
clockAtom.onMount = (setAtom) => {
const interval = setInterval(() => {
setAtom(new Date())
}, 1000)
return () => clearInterval(interval)
}
Type Definitions
Core Atom Types
export interface Atom<Value> {
toString: () => string
read: Read<Value>
debugLabel?: string
}
export interface WritableAtom<Value, Args extends unknown[], Result>
extends Atom<Value> {
read: Read<Value>
write: Write<Args, Result>
onMount?: OnMount<Args, Result>
}
export type PrimitiveAtom<Value> = WritableAtom<
Value,
[SetStateAction<Value>],
void
>
Best Practices
Keep atoms small and focused. Create derived atoms to compute values rather than storing computed values in primitive atoms.
Use TypeScript to ensure type safety. Jotai has excellent TypeScript support with full type inference.
Don’t mutate atom values directly. Always use the setter function to update atom state.
Examples
Counter with Increment/Decrement
import { atom } from 'jotai'
const countAtom = atom(0)
const incrementAtom = atom(
null,
(get, set) => set(countAtom, get(countAtom) + 1)
)
const decrementAtom = atom(
null,
(get, set) => set(countAtom, get(countAtom) - 1)
)
Todo List Filter
import { atom } from 'jotai'
import type { PrimitiveAtom } from 'jotai'
type Todo = { title: string; completed: boolean }
const filterAtom = atom<'all' | 'completed' | 'incompleted'>('all')
const todosAtom = atom<PrimitiveAtom<Todo>[]>([])
const filteredTodosAtom = atom((get) => {
const filter = get(filterAtom)
const todos = get(todosAtom)
if (filter === 'all') return todos
if (filter === 'completed') {
return todos.filter((atom) => get(atom).completed)
}
return todos.filter((atom) => !get(atom).completed)
})