Skip to main content
freezeAtom wraps an atom to deeply freeze its values in development, helping catch mutations.

Import

import { freezeAtom } from 'jotai/utils'

Signature

function freezeAtom<AtomType extends Atom<unknown>>(
  anAtom: AtomType,
): AtomType

Parameters

anAtom
AtomType extends Atom<unknown>
required
The atom to freeze. Its values will be deeply frozen using Object.freeze

Return Value

Returns the same atom instance with modified read/write functions that:
  • Deep freeze values when reading
  • Deep freeze values before setting (for writable atoms)

Usage Example

import { atom } from 'jotai'
import { freezeAtom } from 'jotai/utils'

const objectAtom = freezeAtom(
  atom({ count: 0, nested: { value: 'test' } })
)

function Component() {
  const [obj, setObj] = useAtom(objectAtom)

  // This will throw an error in development
  // obj.count++ // Error: Cannot assign to read only property

  // Correct way: create a new object
  const increment = () => {
    setObj({ ...obj, count: obj.count + 1 })
  }

  return (
    <div>
      <p>Count: {obj.count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  )
}

Array Example

import { atom } from 'jotai'
import { freezeAtom } from 'jotai/utils'

const todosAtom = freezeAtom(
  atom([
    { id: 1, text: 'Learn Jotai', completed: false },
    { id: 2, text: 'Build app', completed: false },
  ])
)

function TodoList() {
  const [todos, setTodos] = useAtom(todosAtom)

  const toggleTodo = (id: number) => {
    // This will throw in development:
    // const todo = todos.find(t => t.id === id)
    // todo.completed = !todo.completed // Error!

    // Correct way:
    setTodos(
      todos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    )
  }

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id} onClick={() => toggleTodo(todo.id)}>
          {todo.text}
        </li>
      ))}
    </ul>
  )
}

Nested Objects

import { atom } from 'jotai'
import { freezeAtom } from 'jotai/utils'

const userAtom = freezeAtom(
  atom({
    profile: {
      name: 'Alice',
      settings: {
        theme: 'dark',
        notifications: true,
      },
    },
  })
)

function UserSettings() {
  const [user, setUser] = useAtom(userAtom)

  const updateTheme = (theme: string) => {
    // Deep freeze prevents this:
    // user.profile.settings.theme = theme // Error!

    // Must create new objects at each level:
    setUser({
      ...user,
      profile: {
        ...user.profile,
        settings: {
          ...user.profile.settings,
          theme,
        },
      },
    })
  }

  return (
    <div>
      <p>Theme: {user.profile.settings.theme}</p>
      <button onClick={() => updateTheme('light')}>Light Mode</button>
    </div>
  )
}

Catching Bugs in Development

import { atom } from 'jotai'
import { freezeAtom } from 'jotai/utils'

interface CartItem {
  id: number
  quantity: number
}

const cartAtom = freezeAtom(atom<CartItem[]>([]))

function Cart() {
  const [cart, setCart] = useAtom(cartAtom)

  const addItem = (item: CartItem) => {
    // BUG: This mutates the array and will throw in development
    // cart.push(item) // Error: Cannot add property, object is not extensible

    // Correct:
    setCart([...cart, item])
  }

  const updateQuantity = (id: number, quantity: number) => {
    // BUG: This mutates an object and will throw
    // const item = cart.find(i => i.id === id)
    // if (item) item.quantity = quantity // Error!

    // Correct:
    setCart(
      cart.map((item) => (item.id === id ? { ...item, quantity } : item))
    )
  }

  return <div>{/* ... */}</div>
}

With Derived Atoms

import { atom } from 'jotai'
import { freezeAtom } from 'jotai/utils'

const baseAtom = freezeAtom(
  atom({ items: [1, 2, 3], metadata: { total: 3 } })
)

const totalAtom = atom((get) => {
  const base = get(baseAtom)
  // base is frozen, preventing accidental mutations
  return base.items.reduce((sum, item) => sum + item, 0)
})

Deprecated: freezeAtomCreator

freezeAtomCreator is deprecated. Define the wrapper on the user end instead.
// Deprecated
import { freezeAtomCreator } from 'jotai/utils'

// Instead, create your own wrapper:
const createFrozenAtom = <T>(initialValue: T) => {
  return freezeAtom(atom(initialValue))
}

Notes

  • Deep freezing is a development-time debugging aid and has performance implications
  • Only use in development environments to catch mutation bugs
  • Object.freeze is applied recursively to all nested objects and arrays
  • Frozen atoms enforce immutability and help identify code that incorrectly mutates state
  • The atom reference itself is modified in place; the same atom instance is returned
  • If the atom is already frozen, calling freezeAtom again has no effect
  • Primitives (strings, numbers, booleans) are inherently immutable and unaffected

Build docs developers (and LLMs) love