How to use Jotai in React
Jotai is an atomic state management library for React that stores state as small, composable atoms — individual pieces of state that components subscribe to directly, re-rendering only when that specific atom changes.
As the creator of CoreUI with 25 years of front-end development experience, I use Jotai when an application needs fine-grained reactive state updates with minimal overhead and no external state management infrastructure.
Jotai atoms are simpler than Redux slices and more granular than Zustand stores, making them ideal for UI state like filter values, modal open/close, or selected items in a list.
The API is intentionally minimal — just atom() and useAtom().
Create atoms and use them in components.
// atoms/filterAtoms.js
import { atom } from 'jotai'
// Primitive atoms
export const searchAtom = atom('')
export const categoryAtom = atom('all')
export const pageAtom = atom(1)
// Derived atom - computed from other atoms
export const activeFiltersAtom = atom((get) => {
const search = get(searchAtom)
const category = get(categoryAtom)
return {
hasFilters: search !== '' || category !== 'all',
count: (search ? 1 : 0) + (category !== 'all' ? 1 : 0)
}
})
// ProductList.jsx
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
import { searchAtom, categoryAtom, activeFiltersAtom } from './atoms/filterAtoms'
export function SearchBar() {
const [search, setSearch] = useAtom(searchAtom)
return (
<input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Search..."
/>
)
}
export function CategoryFilter() {
const [category, setCategory] = useAtom(categoryAtom)
return (
<select value={category} onChange={e => setCategory(e.target.value)}>
<option value="all">All</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
</select>
)
}
export function FilterBadge() {
// useAtomValue - read only, no setter
const { hasFilters, count } = useAtomValue(activeFiltersAtom)
if (!hasFilters) return null
return <span className="badge">{count} filters active</span>
}
export function ResetButton() {
// useSetAtom - write only, no subscription, no re-renders
const setSearch = useSetAtom(searchAtom)
const setCategory = useSetAtom(categoryAtom)
const setPage = useSetAtom(pageAtom)
function reset() {
setSearch('')
setCategory('all')
setPage(1)
}
return <button onClick={reset}>Reset Filters</button>
}
useAtomValue subscribes read-only — the component re-renders when the atom changes but gets no setter. useSetAtom writes without subscribing — ResetButton never re-renders when filter values change, only when its own parent re-renders.
Async Atoms
Load data with async atoms that integrate with React Suspense.
import { atom } from 'jotai'
import { loadable } from 'jotai/utils'
export const userIdAtom = atom(1)
// Async atom that fetches based on another atom
export const userAtom = atom(async (get) => {
const id = get(userIdAtom)
const res = await fetch(`/api/users/${id}`)
return res.json()
})
// loadable wrapper to avoid Suspense boundary
export const userLoadableAtom = loadable(userAtom)
// UserProfile.jsx
import { useAtomValue, useSetAtom } from 'jotai'
import { userIdAtom, userLoadableAtom } from './atoms/userAtoms'
function UserProfile() {
const userLoadable = useAtomValue(userLoadableAtom)
const setUserId = useSetAtom(userIdAtom)
if (userLoadable.state === 'loading') return <p>Loading...</p>
if (userLoadable.state === 'hasError') return <p>Error</p>
const user = userLoadable.data
return <div>{user.name}</div>
}
loadable converts an async atom into a synchronous atom that returns { state: 'loading' | 'hasData' | 'hasError', data }, avoiding the need for Suspense boundaries.
Best Practice Note
This is the same atomic state approach we recommend in CoreUI React templates for UI state. Jotai atoms are stored in a global WeakMap — no Provider is needed in most cases. For complex application state with many interdependent slices, Zustand or Redux Toolkit may be more appropriate. See how to use Zustand in React for comparison with a store-based approach.



