Jotai provides powerful debugging tools that integrate with both React DevTools and Redux DevTools. This guide covers debugging strategies and tools.
Debug Labels
Debug labels help identify atoms in DevTools.
Manual Debug Labels
import { atom } from 'jotai'
const countAtom = atom(0)
if (process.env.NODE_ENV !== 'production') {
countAtom.debugLabel = 'count'
}
Automatic Debug Labels
Use Babel or SWC plugins to automatically add debug labels:
Install plugin
npm install --save-dev jotai-babel
Configure Babel
// babel.config.js
module.exports = {
plugins: ['jotai-babel/plugin-debug-label']
}
Or configure SWC
// .swcrc
{
"jsc": {
"experimental": {
"plugins": [['@swc-jotai/debug-label', {}]]
}
}
}
The plugin automatically transforms:
// You write:
const countAtom = atom(0)
// Plugin adds:
const countAtom = atom(0)
countAtom.debugLabel = 'countAtom'
Inspect atoms using React DevTools.
useAtom Hook Inspection
Select a component using Jotai atoms in React DevTools. You’ll see “Atom” hooks with their current values:
function Counter() {
const [count] = useAtom(countAtom) // Shows "Atom: count = 0"
const [double] = useAtom(doubleAtom) // Shows "Atom: double = 0"
}
useAtomsDebugValue
See all atoms in the component tree:
import { useAtomsDebugValue } from 'jotai-devtools/utils'
import { Provider } from 'jotai'
function DebugAtoms() {
useAtomsDebugValue()
return null
}
function App() {
return (
<Provider>
<DebugAtoms />
<YourApp />
</Provider>
)
}
In React DevTools, navigate to the DebugAtoms component to see all atom values and their dependents.
Use Redux DevTools for advanced debugging features.
Debug a specific atom:
import { useAtomDevtools } from 'jotai-devtools/utils'
import { useAtom, atom } from 'jotai'
const countAtom = atom(0)
countAtom.debugLabel = 'count'
function Counter() {
const [count, setCount] = useAtom(countAtom)
// Attach to Redux DevTools
useAtomDevtools(countAtom)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
)
}
Debug all atoms in the store:
import { useAtomsDevtools } from 'jotai-devtools/utils'
import { Provider } from 'jotai'
function DebugAtoms() {
useAtomsDevtools('demo')
return null
}
function App() {
return (
<Provider>
<DebugAtoms />
<YourApp />
</Provider>
)
}
Redux DevTools will show:
{
"values": {
"atom1:count": 0,
"atom2:doubleCount": 0,
"atom3:half": 0
},
"dependents": {
"atom1:count": ["atom2:doubleCount"],
"atom2:doubleCount": ["atom3:half"],
"atom3:half": []
}
}
Time Travel
Jump to any previous state:
- Open Redux DevTools
- Click on any action in the history
- Hover over an action
- Click “Jump” to restore that state
Pause Recording
Pause state recording:
- Open Redux DevTools
- Click the pause button
- Changes won’t be recorded until you resume
Dispatch Actions
Manually set atom values:
- Open Redux DevTools
- Click “Show Dispatcher”
- Enter new value (will be parsed with
JSON.parse)
- Click “Dispatch”
Example:
This sets the atom value to 5.
Values must be valid JSON. Complex values like functions cannot be dispatched.
Frozen Atoms
Prevent accidental mutations with freezeAtom:
freezeAtom
import { atom } from 'jotai'
import { freezeAtom } from 'jotai/utils'
const objAtom = freezeAtom(atom({ count: 0, name: 'test' }))
function Component() {
const [obj, setObj] = useAtom(objAtom)
// This will throw an error in development
obj.count = 1 // Error: Cannot assign to read only property
// Correct way: create new object
setObj({ ...obj, count: 1 })
}
freezeAtomCreator
Create a factory for frozen atoms:
import { atom } from 'jotai'
import { freezeAtomCreator } from 'jotai/utils'
const freezeAtom = freezeAtomCreator(atom)
const userAtom = freezeAtom({ id: '1', name: 'John' })
const settingsAtom = freezeAtom({ theme: 'dark' })
// All atoms are frozen
Logging Atom Changes
Log atom changes for debugging:
import { atom, useAtom } from 'jotai'
const countAtom = atom(0)
const countAtomWithLog = atom(
(get) => get(countAtom),
(get, set, newValue) => {
console.log('Count changed from', get(countAtom), 'to', newValue)
set(countAtom, newValue)
}
)
function Counter() {
const [count, setCount] = useAtom(countAtomWithLog)
// Every change is logged
}
Visual Debugger
Use the Jotai DevTools package for a visual debugger:
npm install jotai-devtools
import { DevTools } from 'jotai-devtools'
import 'jotai-devtools/styles.css'
function App() {
return (
<>
<DevTools />
<YourApp />
</>
)
}
This provides:
- Visual atom graph
- Atom value inspector
- Time travel debugging
- Atom dependency visualization
Debugging Async Atoms
Debug async atoms with loading and error states:
import { atom, useAtom } from 'jotai'
import { loadable } from 'jotai/utils'
const asyncAtom = atom(async () => {
const res = await fetch('/api/data')
return res.json()
})
const loadableAtom = loadable(asyncAtom)
function Component() {
const [loadable] = useAtom(loadableAtom)
console.log('State:', loadable.state) // 'loading' | 'hasData' | 'hasError'
if (loadable.state === 'loading') {
console.log('Loading...')
}
if (loadable.state === 'hasData') {
console.log('Data:', loadable.data)
}
if (loadable.state === 'hasError') {
console.error('Error:', loadable.error)
}
}
Tips
Use debug labels in production too. They help debug production issues without impacting performance.
Combine React DevTools for quick inspection and Redux DevTools for advanced debugging like time travel.
Use freezeAtom during development to catch accidental mutations early.
useAtomsDevtools shows atom dependencies, helping you understand your atom graph and optimize re-renders.