Skip to main content
The useAtom hook is the primary way to interact with atoms in React components. It returns a tuple with the atom’s current value and a function to update it, similar to React.useState.

Basic Usage

import { atom, useAtom } from 'jotai'

const countAtom = atom(0)

function Counter() {
  const [count, setCount] = useAtom(countAtom)
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(c => c - 1)}>Decrement</button>
    </div>
  )
}

Type Signature

export function useAtom<Value, Args extends unknown[], Result>(
  atom: WritableAtom<Value, Args, Result>,
  options?: Options,
): [Awaited<Value>, SetAtom<Args, Result>]

export function useAtom<Value>(
  atom: Atom<Value>,
  options?: Options,
): [Awaited<Value>, never]
The hook returns:
  • The atom’s current value (unwrapped if it’s a Promise)
  • A setter function that accepts the atom’s write arguments

Implementation

Internally, useAtom combines useAtomValue and useSetAtom:
export function useAtom<Value, Args extends unknown[], Result>(
  atom: Atom<Value> | WritableAtom<Value, Args, Result>,
  options?: Options,
) {
  return [
    useAtomValue(atom, options),
    useSetAtom(atom as WritableAtom<Value, Args, Result>, options),
  ]
}

Setter Functions

Direct Values

Set the atom to a new value directly:
const [count, setCount] = useAtom(countAtom)
setCount(5) // count is now 5

Updater Functions

Pass a function that receives the previous value:
const [count, setCount] = useAtom(countAtom)
setCount(c => c + 1) // Increment by 1

Custom Arguments

For derived writable atoms with custom write functions:
const incrementByAtom = atom(
  (get) => get(countAtom),
  (get, set, by: number) => set(countAtom, get(countAtom) + by)
)

function Counter() {
  const [count, incrementBy] = useAtom(incrementByAtom)
  
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => incrementBy(5)}>+5</button>
      <button onClick={() => incrementBy(10)}>+10</button>
    </div>
  )
}

Read-Only Atoms

When using useAtom with a read-only atom, the setter will be typed as never:
const doubleCountAtom = atom((get) => get(countAtom) * 2)

function DoubleCounter() {
  const [doubleCount] = useAtom(doubleCountAtom)
  // No setter available for read-only atoms
  
  return <p>Double: {doubleCount}</p>
}
For read-only atoms, use useAtomValue instead of useAtom for clearer intent.

Write-Only Atoms

For write-only atoms (created with atom(null, writeFunction)), the value will be null:
const incrementAtom = atom(
  null,
  (get, set) => set(countAtom, get(countAtom) + 1)
)

function IncrementButton() {
  const [, increment] = useAtom(incrementAtom)
  
  return <button onClick={increment}>Increment</button>
}
For write-only atoms, use useSetAtom instead of useAtom to avoid the unused value.

Options

store

Use a specific store instead of the one from Provider:
import { createStore, useAtom } from 'jotai'

const myStore = createStore()

function Counter() {
  const [count, setCount] = useAtom(countAtom, { store: myStore })
  return <div>{count}</div>
}

useAtomValue

Use useAtomValue when you only need to read the atom’s value:
import { useAtomValue } from 'jotai'

function Display() {
  const count = useAtomValue(countAtom)
  return <p>{count}</p>
}
This prevents unnecessary re-renders when you don’t need the setter.

useSetAtom

Use useSetAtom when you only need to update the atom:
import { useSetAtom } from 'jotai'

function IncrementButton() {
  const setCount = useSetAtom(countAtom)
  return <button onClick={() => setCount(c => c + 1)}>+</button>
}
This prevents re-renders when the atom’s value changes.
The examples in the Jotai source code often have comments like:
// Use `useSetAtom` to avoid re-render
// const [, setPostId] = useAtom(postId)
const setPostId = useSetAtom(postId)
This optimization is important for performance in large applications.

Component Re-renders

useAtom subscribes your component to the atom. The component will re-render whenever:
  • The atom’s value changes
  • Any atoms that the atom depends on change (for derived atoms)
const countAtom = atom(0)
const doubleAtom = atom((get) => get(countAtom) * 2)

function Display() {
  const [double] = useAtom(doubleAtom)
  console.log('Display rendered')
  return <p>{double}</p>
}

function Controls() {
  const [, setCount] = useAtom(countAtom)
  // This component won't re-render when countAtom changes
  return <button onClick={() => setCount(c => c + 1)}>+</button>
}

Best Practices

Split useAtom into useAtomValue and useSetAtom when you only need one or the other. This prevents unnecessary re-renders.
Use TypeScript to get full type inference for setter arguments in custom writable atoms.
Don’t destructure atoms before passing them to hooks:
// ❌ Bad
const { value, setValue } = useAtom(atom)

// ✅ Good
const [value, setValue] = useAtom(atom)

Examples

Form Input

import { atom, useAtom } from 'jotai'

const nameAtom = atom('')
const emailAtom = atom('')

function Form() {
  const [name, setName] = useAtom(nameAtom)
  const [email, setEmail] = useAtom(emailAtom)
  
  return (
    <form>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Name"
      />
      <input
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
        type="email"
      />
    </form>
  )
}

Toggle

import { atom, useAtom } from 'jotai'

const isDarkModeAtom = atom(false)
const toggleDarkModeAtom = atom(
  (get) => get(isDarkModeAtom),
  (get, set) => set(isDarkModeAtom, !get(isDarkModeAtom))
)

function ThemeToggle() {
  const [isDark, toggle] = useAtom(toggleDarkModeAtom)
  
  return (
    <button onClick={toggle}>
      {isDark ? '🌙 Dark' : '☀️ Light'}
    </button>
  )
}

Counter with Min/Max

import { atom, useAtom } from 'jotai'

const countAtom = atom(0)
const MIN = 0
const MAX = 10

const boundedCountAtom = atom(
  (get) => get(countAtom),
  (get, set, newValue: number) => {
    set(countAtom, Math.max(MIN, Math.min(MAX, newValue)))
  }
)

function BoundedCounter() {
  const [count, setCount] = useAtom(boundedCountAtom)
  
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count - 1)}>-</button>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  )
}

Shopping Cart

import { atom, useAtom } from 'jotai'

type CartItem = { id: number; name: string; quantity: number }

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

const addToCartAtom = atom(
  null,
  (get, set, item: Omit<CartItem, 'quantity'>) => {
    const cart = get(cartAtom)
    const existing = cart.find(i => i.id === item.id)
    
    if (existing) {
      set(cartAtom, cart.map(i =>
        i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
      ))
    } else {
      set(cartAtom, [...cart, { ...item, quantity: 1 }])
    }
  }
)

function AddToCart({ product }) {
  const [, addToCart] = useAtom(addToCartAtom)
  
  return (
    <button onClick={() => addToCart(product)}>
      Add to Cart
    </button>
  )
}

function Cart() {
  const [cart] = useAtom(cartAtom)
  
  return (
    <div>
      {cart.map(item => (
        <div key={item.id}>
          {item.name} x {item.quantity}
        </div>
      ))}
    </div>
  )
}

Build docs developers (and LLMs) love