Skip to main content
loadable is deprecated in favor of unwrap. It will be removed in v3.
loadable wraps an atom to convert async values and errors into a loadable state object.

Import

import { loadable } from 'jotai/utils'

Signature

function loadable<Value>(
  anAtom: Atom<Value>,
): Atom<Loadable<Value>>

type Loadable<Value> =
  | { state: 'loading' }
  | { state: 'hasError'; error: unknown }
  | { state: 'hasData'; data: Awaited<Value> }

Parameters

anAtom
Atom<Value>
required
The atom to wrap. Can be synchronous or asynchronous

Return Value

Returns a read-only atom that contains a loadable state object with one of three states:
  • { state: 'loading' } - Async value is pending
  • { state: 'hasError', error } - Async value threw an error
  • { state: 'hasData', data } - Value is available

Migration to unwrap

// Old (deprecated)
import { loadable } from 'jotai/utils'
const loadableAtom = loadable(asyncAtom)

// New (recommended)
import { unwrap } from 'jotai/utils'

function loadable<Value>(anAtom: Atom<Value>) {
  const LOADING = { state: 'loading' as const }
  const unwrappedAtom = unwrap(anAtom, () => LOADING)
  return atom((get) => {
    try {
      const data = get(unwrappedAtom)
      if (data === LOADING) {
        return LOADING
      }
      return { state: 'hasData' as const, data }
    } catch (error) {
      return { state: 'hasError' as const, error }
    }
  })
}

Usage Example (Deprecated)

import { atom, useAtom } from 'jotai'
import { loadable } from 'jotai/utils'

const userAtom = atom(async () => {
  const response = await fetch('/api/user')
  if (!response.ok) throw new Error('Failed to fetch')
  return response.json()
})

const userLoadableAtom = loadable(userAtom)

function UserProfile() {
  const [userLoadable] = useAtom(userLoadableAtom)

  if (userLoadable.state === 'loading') {
    return <div>Loading...</div>
  }

  if (userLoadable.state === 'hasError') {
    return <div>Error: {userLoadable.error.message}</div>
  }

  // userLoadable.state === 'hasData'
  return <div>User: {userLoadable.data.name}</div>
}

Pattern Matching

import { loadable } from 'jotai/utils'

const dataLoadableAtom = loadable(asyncDataAtom)

function DataDisplay() {
  const [data] = useAtom(dataLoadableAtom)

  return (
    <div>
      {data.state === 'loading' && <Spinner />}
      {data.state === 'hasError' && <ErrorMessage error={data.error} />}
      {data.state === 'hasData' && <DataView data={data.data} />}
    </div>
  )
}

Helper Function

import { loadable, type Loadable } from 'jotai/utils'

function matchLoadable<T, R>(
  loadable: Loadable<T>,
  handlers: {
    loading: () => R
    hasError: (error: unknown) => R
    hasData: (data: Awaited<T>) => R
  }
): R {
  switch (loadable.state) {
    case 'loading':
      return handlers.loading()
    case 'hasError':
      return handlers.hasError(loadable.error)
    case 'hasData':
      return handlers.hasData(loadable.data)
  }
}

function Component() {
  const [loadable] = useAtom(loadableAtom)

  return matchLoadable(loadable, {
    loading: () => <div>Loading...</div>,
    hasError: (error) => <div>Error: {String(error)}</div>,
    hasData: (data) => <div>Data: {data}</div>,
  })
}

Notes

  • Deprecated: Use unwrap instead for new code
  • Provides a way to handle async states without Suspense
  • Does not throw or suspend, making it useful for optional error boundaries
  • The loadable atom never throws; errors are captured in the state
  • TypeScript discriminated union makes pattern matching type-safe
  • See the migration guide above for userland implementation using unwrap

Build docs developers (and LLMs) love