The “atoms in atom” pattern involves storing atom configs as values in other atoms. This enables powerful dynamic state patterns where the state structure itself can change at runtime.
When to Use This Pattern
Use atoms in atom when:
- You need dynamically created state (e.g., todo items, form fields)
- You want to avoid re-rendering unrelated components
- You’re building reusable components with isolated state
- You need referential equality for React keys
Advanced pattern: This pattern has a learning curve and can make state harder to debug. Use it only when you need its specific benefits.
Understanding Atom Configs
First, understand that atom() creates a config object, not state:
const countAtom = atom(0)
// countAtom is just a config object
// Atoms are identified by referential equality (like object keys)
This means you can:
- Store atoms in variables
- Pass atoms as props
- Store atoms in other atoms
- Use atoms as Map/Set keys
Storing Atoms in useState
You can store atom configs in component state:
import { useState } from 'react'
import { atom, useAtom } from 'jotai'
function DynamicCounter() {
// Create an atom on demand
const [currentAtom, setCurrentAtom] = useState(() => atom(0))
const [count, setCount] = useAtom(currentAtom)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>+1</button>
<button onClick={() => setCurrentAtom(atom(0))}>
Create new counter
</button>
</div>
)
}
When you create a new atom with atom(0), you’re creating a completely separate state. The old atom’s state is garbage collected if nothing references it.
Storing Atoms in Atoms
You can store atom configs as values:
import { atom, useAtom } from 'jotai'
const firstNameAtom = atom('Tanjiro')
const lastNameAtom = atom('Kamado')
// This atom stores an atom config as its value
const selectedNameAtom = atom(firstNameAtom)
function NameDisplay() {
const [nameAtom, setNameAtom] = useAtom(selectedNameAtom)
const [name] = useAtom(nameAtom)
return (
<div>
<p>Name: {name}</p>
<button onClick={() => setNameAtom(firstNameAtom)}>
Show First Name
</button>
<button onClick={() => setNameAtom(lastNameAtom)}>
Show Last Name
</button>
</div>
)
}
Derived Atoms with Double Get
You can create derived atoms that read from dynamic atoms:
// Read the atom stored in selectedNameAtom, then read that atom's value
const derivedNameAtom = atom((get) => {
const nameAtom = get(selectedNameAtom)
return get(nameAtom)
})
// Shorter version
const derivedNameAtom = atom((get) => get(get(selectedNameAtom)))
TypeScript tip: Use explicit naming to avoid confusion about what’s an atom and what’s a value. E.g., nameAtom vs name.
Array of Atoms Pattern
This is the classic “atoms in atom” pattern—storing an array of atoms:
import { atom, useAtom } from 'jotai'
// Array of atom configs
const todosAtom = atom([
atom({ text: 'Buy milk', done: false }),
atom({ text: 'Walk dog', done: false }),
atom({ text: 'Write code', done: true })
])
function TodoItem({ todoAtom }) {
const [todo, setTodo] = useAtom(todoAtom)
return (
<div>
<input
type="checkbox"
checked={todo.done}
onChange={(e) => setTodo({ ...todo, done: e.target.checked })}
/>
<span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
{todo.text}
</span>
</div>
)
}
function TodoList() {
const [todos, setTodos] = useAtom(todosAtom)
const addTodo = () => {
const newTodoAtom = atom({ text: 'New todo', done: false })
setTodos((prev) => [...prev, newTodoAtom])
}
const removeTodo = (todoAtom) => {
setTodos((prev) => prev.filter((atom) => atom !== todoAtom))
}
return (
<div>
{todos.map((todoAtom) => (
<TodoItem
key={`${todoAtom}`} // atom.toString() returns unique ID
todoAtom={todoAtom}
/>
))}
<button onClick={addTodo}>Add Todo</button>
</div>
)
}
Why This Pattern is Powerful
Performance benefit: When you update one todo item, only that TodoItem re-renders:
// Updating todoAtom[0] only re-renders TodoItem for that atom
store.set(todoAtom, { text: 'Buy milk', done: true })
// TodoList and other TodoItems don't re-render!
Without atoms in atom:
// Traditional approach - entire TodoList re-renders
const todosAtom = atom([
{ id: 1, text: 'Buy milk', done: false },
{ id: 2, text: 'Walk dog', done: false }
])
// Updating one todo causes all todo items to re-render
Map of Atoms Pattern
You can also use objects/maps:
const pricesAtom = atom({
apple: atom(15),
orange: atom(12),
pineapple: atom(25)
})
function Fruit({ name, priceAtom }) {
const [price, setPrice] = useAtom(priceAtom)
return (
<div>
{name}: ${price}
<button onClick={() => setPrice((p) => p + 1)}>+</button>
</div>
)
}
function PriceList() {
const [prices] = useAtom(pricesAtom)
return (
<div>
{Object.keys(prices).map((name) => (
<Fruit
key={name}
name={name}
priceAtom={prices[name]}
/>
))}
</div>
)
}
Dynamic Creation Patterns
Creating Atoms On-Demand
import { atom, useAtom } from 'jotai'
import { atomFamily } from 'jotai/utils'
// Create atoms on demand by ID
const todoAtomFamily = atomFamily((id) =>
atom({ id, text: '', done: false })
)
function TodoById({ id }) {
const [todo] = useAtom(todoAtomFamily(id))
return <div>{todo.text}</div>
}
atomFamily from jotai/utils is a helper for this pattern. It memoizes atom creation.
const formFieldsAtom = atom({
email: atom(''),
password: atom(''),
confirmPassword: atom('')
})
function FormField({ name, fieldAtom }) {
const [value, setValue] = useAtom(fieldAtom)
return (
<input
type={name === 'email' ? 'email' : 'password'}
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder={name}
/>
)
}
function Form() {
const [fields] = useAtom(formFieldsAtom)
return (
<form>
{Object.entries(fields).map(([name, fieldAtom]) => (
<FormField key={name} name={name} fieldAtom={fieldAtom} />
))}
</form>
)
}
Memory Usage
Each atom has its own state in the store:
// 100 items = 100 atom states in the store
const itemsAtom = atom(
Array.from({ length: 100 }, () => atom({ data: 'value' }))
)
This is fine for hundreds of items, but thousands might need consideration.
Garbage Collection
Unreferenced atoms are garbage collected:
const [todos, setTodos] = useAtom(todosAtom)
// Remove a todo - its atom is GC'd if nothing else references it
setTodos((prev) => prev.filter((atom) => atom !== atomToRemove))
Re-render Optimization
This pattern shines with many items:
// With atoms in atom: Only changed item re-renders
// Without: All items re-render on any change
// 1000 todos, update one:
// - Atoms in atom: 1 component re-renders
// - Traditional: 1000+ components re-render
Edge Cases and Gotchas
Don’t Use Atoms as Primitives
// Don't do this - atoms aren't comparable
const countAtom = atom(0)
const isZeroAtom = atom((get) => get(countAtom) === 0) // ✅ Good
// Comparing atoms themselves is confusing
const selectedAtom = atom(null)
const isSelectedAtom = atom((get) => get(selectedAtom) === countAtom) // ❌ Confusing
TypeScript Complexity
Type inference can be tricky:
import { Atom, PrimitiveAtom } from 'jotai'
// Explicit types help
const todosAtom: PrimitiveAtom<PrimitiveAtom<Todo>[]> = atom([
atom({ text: 'Buy milk', done: false })
])
// Or use type helper
type TodoAtom = PrimitiveAtom<Todo>
type TodosAtom = PrimitiveAtom<TodoAtom[]>
Atom Identity
Atoms are identified by reference:
// These are different atoms!
const atom1 = atom(0)
const atom2 = atom(0)
atom1 !== atom2 // true
// This creates a new atom every render - don't do this!
function Component() {
const [todos, setTodos] = useAtom(
atom([atom(0)]) // ❌ New atom every render!
)
}
// Define atoms outside components or use useMemo
const todosAtom = atom([atom(0)]) // ✅ Stable reference
Using toString() for Keys
Atoms have a unique string representation:
{todos.map((todoAtom) => (
<TodoItem key={`${todoAtom}`} atom={todoAtom} />
// or: key={todoAtom.toString()}
))}
Best Practices
- Name clearly: Use
Atom suffix (e.g., todoAtom, selectedAtom)
- Define outside components: Avoid creating atoms inside render
- Use atomFamily: For ID-based atom creation
- Document structure: Complex nesting needs good comments
- Consider alternatives: Only use when you need the performance benefit
When Not to Use
- Simple lists with few items (< 50)
- Read-only data
- Data that changes all at once
- When debugging/tracing is more important than performance
Example: Kanban Board
import { atom, useAtom } from 'jotai'
// Each card is an atom
const cardsAtom = atom({
todo: [
atom({ id: 1, text: 'Design API' }),
atom({ id: 2, text: 'Write docs' })
],
doing: [
atom({ id: 3, text: 'Implement feature' })
],
done: [
atom({ id: 4, text: 'Fix bug' })
]
})
function Card({ cardAtom }) {
const [card, setCard] = useAtom(cardAtom)
return (
<div className="card">
<input
value={card.text}
onChange={(e) => setCard({ ...card, text: e.target.value })}
/>
</div>
)
}
function Column({ title, cardAtoms }) {
return (
<div className="column">
<h2>{title}</h2>
{cardAtoms.map((cardAtom) => (
<Card key={`${cardAtom}`} cardAtom={cardAtom} />
))}
</div>
)
}
function KanbanBoard() {
const [cards] = useAtom(cardsAtom)
return (
<div className="board">
<Column title="To Do" cardAtoms={cards.todo} />
<Column title="Doing" cardAtoms={cards.doing} />
<Column title="Done" cardAtoms={cards.done} />
</div>
)
}
Learn More