freezeAtom wraps an atom to deep freeze its values, preventing accidental mutations in development. This helps catch bugs where state is mutated instead of being updated immutably.
Signature
function freezeAtom<AtomType extends Atom<unknown>>(
anAtom: AtomType
): AtomType
The atom to wrap with deep freezing
Usage
Basic usage
import { freezeAtom } from 'jotai/utils'
import { atom, useAtom } from 'jotai'
const baseAtom = atom({ count: 0, items: [] })
const frozenAtom = freezeAtom(baseAtom)
function Counter() {
const [state, setState] = useAtom(frozenAtom)
const handleIncrement = () => {
// ❌ This will throw an error in development
// state.count++
// ✅ This is correct - create a new object
setState({ ...state, count: state.count + 1 })
}
return (
<div>
<div>Count: {state.count}</div>
<button onClick={handleIncrement}>Increment</button>
</div>
)
}
Catching array mutations
import { freezeAtom } from 'jotai/utils'
import { atom, useAtom } from 'jotai'
const todoListAtom = freezeAtom(
atom([
{ id: 1, text: 'Buy milk', completed: false },
{ id: 2, text: 'Walk dog', completed: true }
])
)
function TodoList() {
const [todos, setTodos] = useAtom(todoListAtom)
const handleAdd = (text: string) => {
// ❌ This will throw an error - can't mutate frozen array
// todos.push({ id: Date.now(), text, completed: false })
// ✅ Correct - create new array
setTodos([...todos, { id: Date.now(), text, completed: false }])
}
const handleToggle = (id: number) => {
// ❌ This will throw an error - can't mutate frozen objects
// const todo = todos.find(t => t.id === id)
// todo.completed = !todo.completed
// ✅ Correct - map to new array with new objects
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
))
}
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggle(todo.id)}
/>
{todo.text}
</li>
))}
</ul>
)
}
Nested object mutations
import { freezeAtom } from 'jotai/utils'
import { atom, useAtom } from 'jotai'
const userAtom = freezeAtom(
atom({
name: 'John',
address: {
street: '123 Main St',
city: 'New York',
country: 'USA'
},
preferences: {
theme: 'dark',
notifications: true
}
})
)
function UserProfile() {
const [user, setUser] = useAtom(userAtom)
const updateTheme = (theme: string) => {
// ❌ This will throw an error - nested objects are also frozen
// user.preferences.theme = theme
// ✅ Correct - create new nested objects
setUser({
...user,
preferences: {
...user.preferences,
theme
}
})
}
return (
<div>
<div>Theme: {user.preferences.theme}</div>
<button onClick={() => updateTheme('light')}>Light theme</button>
</div>
)
}
Using with derived atoms
import { freezeAtom } from 'jotai/utils'
import { atom, useAtom } from 'jotai'
const baseDataAtom = freezeAtom(
atom({ users: [], posts: [] })
)
const usersAtom = atom(
(get) => get(baseDataAtom).users,
(get, set, newUsers) => {
set(baseDataAtom, {
...get(baseDataAtom),
users: newUsers
})
}
)
function UserList() {
const [users, setUsers] = useAtom(usersAtom)
const addUser = (user) => {
// ✅ Correct - create new array
setUsers([...users, user])
}
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
Development-only freezing
import { freezeAtom } from 'jotai/utils'
import { atom } from 'jotai'
const createAtom = (initialValue) => {
const baseAtom = atom(initialValue)
// Only freeze in development
return process.env.NODE_ENV === 'development'
? freezeAtom(baseAtom)
: baseAtom
}
const myAtom = createAtom({ count: 0 })
Features
- Deep freezing: Recursively freezes all nested objects and arrays
- Development aid: Catches mutation bugs during development
- Immutability enforcement: Throws errors when trying to mutate frozen values
- Works with all atoms: Can wrap both primitive and derived atoms
- Transparent: Doesn’t change the atom’s type or API
Notes
- The freezing is deep - all nested objects and arrays are frozen
- Frozen objects throw
TypeError when mutation is attempted in strict mode
- In non-strict mode, mutations silently fail
- This is primarily a development tool - consider disabling in production for performance
- Only objects and arrays are frozen; primitives are unchanged
- The atom itself is modified in-place and returned
freezeAtomCreator (deprecated)
freezeAtomCreator is deprecated. Define the wrapper on the user end instead.
// Before (deprecated)
import { freezeAtomCreator } from 'jotai/utils'
import { atom } from 'jotai'
const createFrozenAtom = freezeAtomCreator(atom)
const myAtom = createFrozenAtom({ count: 0 })
// After (recommended)
import { freezeAtom } from 'jotai/utils'
import { atom } from 'jotai'
const createFrozenAtom = (initialValue) => freezeAtom(atom(initialValue))
const myAtom = createFrozenAtom({ count: 0 })
When to use
- During development: To catch accidental mutations
- Complex state: When managing deeply nested state structures
- Team projects: To enforce immutability patterns across the team
- Learning: To understand where mutations occur in your code
- Deep freezing has a runtime cost
- Consider using only in development builds
- For large objects, freezing can be expensive
- The check happens on every read and write