Skip to main content
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)
})

Build docs developers (and LLMs) love