Skip to main content
unwrap creates an atom that unwraps async values and provides a fallback while loading.

Import

import { unwrap } from 'jotai/utils'

Signature

// Read-only with default undefined fallback
function unwrap<Value>(
  anAtom: Atom<Value>,
): Atom<Awaited<Value> | undefined>

// Read-only with custom fallback
function unwrap<Value, PendingValue>(
  anAtom: Atom<Value>,
  fallback: (prev?: Awaited<Value>) => PendingValue,
): Atom<Awaited<Value> | PendingValue>

// Writable with default undefined fallback
function unwrap<Value, Args extends unknown[], Result>(
  anAtom: WritableAtom<Value, Args, Result>,
): WritableAtom<Awaited<Value> | undefined, Args, Result>

// Writable with custom fallback
function unwrap<Value, Args extends unknown[], Result, PendingValue>(
  anAtom: WritableAtom<Value, Args, Result>,
  fallback: (prev?: Awaited<Value>) => PendingValue,
): WritableAtom<Awaited<Value> | PendingValue, Args, Result>

Parameters

anAtom
Atom<Value> | WritableAtom<Value, Args, Result>
required
The atom to unwrap. Can be synchronous or asynchronous
fallback
(prev?: Awaited<Value>) => PendingValue
Optional function that returns a fallback value while the promise is pending. Receives the previous resolved value if available. Defaults to returning undefined

Return Value

Returns an atom that:
  • Returns the resolved value when the promise completes
  • Returns the fallback value while the promise is pending
  • Preserves write functionality for writable atoms
  • Does not suspend or throw

Usage Example

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

const userAtom = atom(async () => {
  const response = await fetch('/api/user')
  return response.json()
})

const userUnwrappedAtom = unwrap(userAtom, () => ({ name: 'Loading...' }))

function UserProfile() {
  const [user] = useAtom(userUnwrappedAtom)
  // No suspense, shows fallback while loading
  return <div>User: {user.name}</div>
}

Default Undefined Fallback

import { unwrap } from 'jotai/utils'

const asyncAtom = atom(async () => {
  await delay(1000)
  return 'Hello'
})

const unwrappedAtom = unwrap(asyncAtom)
// Type: Atom<string | undefined>

function Component() {
  const [value] = useAtom(unwrappedAtom)
  // value is undefined while loading, then 'Hello'
  return <div>{value ?? 'Loading...'}</div>
}

With Previous Value

import { unwrap } from 'jotai/utils'
import { atom } from 'jotai'

const refreshCounterAtom = atom(0)

const dataAtom = atom(async (get) => {
  get(refreshCounterAtom) // dependency for refresh
  const response = await fetch('/api/data')
  return response.json()
})

const unwrappedDataAtom = unwrap(
  dataAtom,
  (prev) => prev ?? { status: 'loading' }
)

function DataDisplay() {
  const [data] = useAtom(unwrappedDataAtom)
  const [, refresh] = useAtom(refreshCounterAtom)

  // Shows previous data while refreshing
  return (
    <div>
      <pre>{JSON.stringify(data, null, 2)}</pre>
      <button onClick={() => refresh((c) => c + 1)}>Refresh</button>
    </div>
  )
}

Writable Unwrapped Atom

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

const asyncCountAtom = atom(
  async () => {
    await delay(100)
    return 0
  },
  async (get, set, newValue: number) => {
    await delay(100)
    set(asyncCountAtom, Promise.resolve(newValue))
  }
)

const unwrappedCountAtom = unwrap(asyncCountAtom, () => 0)

function Counter() {
  const [count, setCount] = useAtom(unwrappedCountAtom)

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  )
}

Loading States

import { unwrap } from 'jotai/utils'

const LOADING = { state: 'loading' as const }

const userAtom = atom(async () => {
  const response = await fetch('/api/user')
  return response.json()
})

const userStateAtom = unwrap(userAtom, () => LOADING)

function UserProfile() {
  const [user] = useAtom(userStateAtom)

  if (user === LOADING) {
    return <div>Loading user...</div>
  }

  return <div>Welcome, {user.name}!</div>
}

Skeleton UI Pattern

import { unwrap } from 'jotai/utils'

interface Post {
  id: number
  title: string
  body: string
}

const postsAtom = atom<Promise<Post[]>>(async () => {
  const response = await fetch('/api/posts')
  return response.json()
})

const unwrappedPostsAtom = unwrap(
  postsAtom,
  (prev) => prev ?? Array(3).fill({ id: 0, title: '...', body: '...' })
)

function PostList() {
  const [posts] = useAtom(unwrappedPostsAtom)

  return (
    <ul>
      {posts.map((post, i) => (
        <li key={post.id || i}>
          <h3>{post.title}</h3>
          <p>{post.body}</p>
        </li>
      ))}
    </ul>
  )
}

Implementing loadable

import { atom } from 'jotai'
import { unwrap } from 'jotai/utils'

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

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 }
    }
  })
}

Error Handling

import { unwrap } from 'jotai/utils'

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

const unwrappedDataAtom = unwrap(dataAtom, () => null)

function DataComponent() {
  try {
    const [data] = useAtom(unwrappedDataAtom)
    if (data === null) return <div>Loading...</div>
    return <div>{JSON.stringify(data)}</div>
  } catch (error) {
    // Errors are still thrown and can be caught
    return <div>Error: {error.message}</div>
  }
}

Notes

  • The unwrapped atom does not suspend; it returns the fallback value instead
  • Errors from the async atom are still thrown when reading the unwrapped atom
  • The fallback function receives the previous resolved value, useful for showing stale data
  • When the promise resolves, the atom updates to the resolved value
  • Writable atoms preserve their write functionality
  • More flexible than the deprecated loadable utility
  • Useful for implementing custom loading states without Suspense

Build docs developers (and LLMs) love