Deprecated: loadable is deprecated in favor of unwrap. See the migration guide below.
loadable creates an atom that wraps an async atom and provides loading, error, and data states without suspending.
Signature
function loadable<Value>(anAtom: Atom<Value>): Atom<Loadable<Value>>
type Loadable<Value> =
| { state: 'loading' }
| { state: 'hasError'; error: unknown }
| { state: 'hasData'; data: Awaited<Value> }
The atom to convert to a loadable atom (typically an async atom)
Usage
Basic async atom without suspense
import { loadable } from 'jotai/utils'
import { atom, useAtomValue } from 'jotai'
const userAtom = atom(async () => {
const response = await fetch('/api/user')
return response.json()
})
const userLoadableAtom = loadable(userAtom)
function User() {
const userLoadable = useAtomValue(userLoadableAtom)
if (userLoadable.state === 'loading') {
return <div>Loading...</div>
}
if (userLoadable.state === 'hasError') {
return <div>Error: {userLoadable.error.message}</div>
}
return <div>Hello, {userLoadable.data.name}!</div>
}
Handling all states
import { loadable } from 'jotai/utils'
import { atom, useAtomValue } from 'jotai'
const dataAtom = atom(async () => {
const response = await fetch('/api/data')
if (!response.ok) {
throw new Error('Failed to fetch')
}
return response.json()
})
const dataLoadableAtom = loadable(dataAtom)
function DataDisplay() {
const loadable = useAtomValue(dataLoadableAtom)
switch (loadable.state) {
case 'loading':
return (
<div className="spinner">
<div>Loading data...</div>
</div>
)
case 'hasError':
return (
<div className="error">
<h3>Error occurred</h3>
<p>{loadable.error?.toString()}</p>
</div>
)
case 'hasData':
return (
<div className="data">
<pre>{JSON.stringify(loadable.data, null, 2)}</pre>
</div>
)
}
}
Multiple loadable atoms
import { loadable } from 'jotai/utils'
import { atom, useAtomValue } from 'jotai'
const userAtom = atom(async () => {
const res = await fetch('/api/user')
return res.json()
})
const postsAtom = atom(async () => {
const res = await fetch('/api/posts')
return res.json()
})
const userLoadableAtom = loadable(userAtom)
const postsLoadableAtom = loadable(postsAtom)
function Dashboard() {
const user = useAtomValue(userLoadableAtom)
const posts = useAtomValue(postsLoadableAtom)
const isLoading = user.state === 'loading' || posts.state === 'loading'
const hasError = user.state === 'hasError' || posts.state === 'hasError'
if (isLoading) {
return <div>Loading...</div>
}
if (hasError) {
return <div>Error loading data</div>
}
return (
<div>
<h1>Welcome, {user.data.name}!</h1>
<h2>Your posts:</h2>
<ul>
{posts.data.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
)
}
Migration to unwrap
The loadable utility is deprecated in favor of unwrap. Here’s how to migrate:
Before (with loadable)
import { loadable } from 'jotai/utils'
import { atom, useAtomValue } from 'jotai'
const dataAtom = atom(async () => fetchData())
const loadableAtom = loadable(dataAtom)
function Component() {
const loadable = useAtomValue(loadableAtom)
if (loadable.state === 'loading') return <div>Loading...</div>
if (loadable.state === 'hasError') return <div>Error</div>
return <div>{loadable.data}</div>
}
After (with unwrap)
import { unwrap } from 'jotai/utils'
import { atom, useAtomValue } from 'jotai'
// Define the loading state
const LOADING = { state: 'loading' } as const
const dataAtom = atom(async () => fetchData())
const unwrappedAtom = unwrap(dataAtom, () => LOADING)
// Wrap in a derived atom to match loadable API
const loadableAtom = atom((get) => {
try {
const data = get(unwrappedAtom)
if (data === LOADING) {
return LOADING
}
return { state: 'hasData', data } as const
} catch (error) {
return { state: 'hasError', error } as const
}
})
function Component() {
const loadable = useAtomValue(loadableAtom)
if (loadable.state === 'loading') return <div>Loading...</div>
if (loadable.state === 'hasError') return <div>Error</div>
return <div>{loadable.data}</div>
}
Or use unwrap directly with simpler state handling:
import { unwrap } from 'jotai/utils'
import { atom, useAtomValue } from 'jotai'
const dataAtom = atom(async () => fetchData())
const unwrappedAtom = unwrap(dataAtom, () => undefined)
function Component() {
const data = useAtomValue(unwrappedAtom)
if (data === undefined) return <div>Loading...</div>
return <div>{data}</div>
}
Features
- No suspense: Doesn’t trigger React Suspense boundaries
- Error handling: Catches errors without Error Boundaries
- Type-safe states: Discriminated union for type-safe state handling
- Progressive loading: Handle loading states manually
Notes
- This utility is deprecated and will be removed in v3
- Use
unwrap for new code (see migration guide above)
- The loadable atom never suspends or throws
- All states are available synchronously
- For sync atoms, the state will always be
hasData