Overview
Async atoms provide a first-class way to work with asynchronous data in TanStack Store. They automatically handle loading, success, and error states, making it easy to build robust applications that fetch and display remote data.
The createAsyncAtom API
The createAsyncAtom function creates a read-only atom that manages asynchronous operations. It returns a special state object that represents the current status of the async operation.
import { createAsyncAtom } from '@tanstack/store'
const userAtom = createAsyncAtom(async () => {
const response = await fetch('/api/user')
return response.json()
})
Async State Structure
Async atoms use a discriminated union type for their state, making it type-safe and easy to handle different states:
type AsyncAtomState<TData, TError = unknown> =
| { status: 'pending' }
| { status: 'done'; data: TData }
| { status: 'error'; error: TError }
Working with Async States
Handling Loading State
When an async atom is first accessed, it starts in the pending state:
const userAtom = createAsyncAtom(async () => {
const response = await fetch('/api/user')
return response.json()
})
const state = userAtom.get()
if (state.status === 'pending') {
console.log('Loading user data...')
}
Handling Success State
Once the promise resolves, the atom transitions to the done state with the returned data:
if (state.status === 'done') {
console.log('User:', state.data)
// state.data is fully typed based on your async function's return type
}
Handling Error State
If the promise rejects, the atom transitions to the error state:
if (state.status === 'error') {
console.error('Failed to load user:', state.error)
}
Real-World Examples
Basic Data Fetching
import { createAsyncAtom } from '@tanstack/store'
interface User {
id: number
name: string
email: string
}
const userAtom = createAsyncAtom<User>(async () => {
const response = await fetch('https://api.example.com/user')
if (!response.ok) {
throw new Error('Failed to fetch user')
}
return response.json()
})
// Subscribe to state changes
userAtom.subscribe((state) => {
switch (state.status) {
case 'pending':
console.log('Loading...')
break
case 'done':
console.log('User loaded:', state.data.name)
break
case 'error':
console.error('Error:', state.error)
break
}
})
Async Atom with UI Components
import { createAsyncAtom } from '@tanstack/store'
import { useStore } from '@tanstack/react-store'
const postsAtom = createAsyncAtom(async () => {
const response = await fetch('https://api.example.com/posts')
return response.json()
})
function PostsList() {
const state = useStore(postsAtom)
if (state.status === 'pending') {
return <div>Loading posts...</div>
}
if (state.status === 'error') {
return <div>Error: {state.error.message}</div>
}
return (
<ul>
{state.data.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
Combining Async Atoms with Derived Stores
You can derive computed values from async atoms:
import { createAsyncAtom, createStore } from '@tanstack/store'
const userAtom = createAsyncAtom(async () => {
const response = await fetch('/api/user')
return response.json()
})
// Create a derived store that extracts just the username
const usernameStore = createStore(() => {
const state = userAtom.get()
if (state.status === 'done') {
return state.data.name
}
return 'Anonymous'
})
console.log(usernameStore.state) // "Anonymous" initially
// After async completes: "John Doe"
Error Handling with Custom Error Types
interface ApiError {
code: string
message: string
}
const dataAtom = createAsyncAtom<Data, ApiError>(async () => {
try {
const response = await fetch('/api/data')
if (!response.ok) {
const error: ApiError = await response.json()
throw error
}
return response.json()
} catch (error) {
if (error instanceof TypeError) {
throw { code: 'NETWORK_ERROR', message: 'Network request failed' }
}
throw error
}
})
dataAtom.subscribe((state) => {
if (state.status === 'error') {
// state.error is typed as ApiError
console.error(`Error ${state.error.code}: ${state.error.message}`)
}
})
Best Practices
Async atoms are read-only. Once created, you cannot manually update their state. To refresh data, create a new async atom or use a regular atom with manual state management.
Pattern: Loading Indicators
Always provide feedback during loading states:
const state = dataAtom.get()
switch (state.status) {
case 'pending':
return <Spinner />
case 'error':
return <ErrorMessage error={state.error} />
case 'done':
return <DataDisplay data={state.data} />
}
Pattern: Retry Logic
Implement retry logic by recreating the async atom:
let retryCount = 0
const maxRetries = 3
const createDataAtom = () => createAsyncAtom(async () => {
try {
const response = await fetch('/api/data')
return response.json()
} catch (error) {
if (retryCount < maxRetries) {
retryCount++
throw error // Will trigger error state, allowing for retry
}
throw new Error(`Failed after ${maxRetries} attempts`)
}
})
For more complex async state management with features like caching, refetching, and mutations, consider using TanStack Query alongside TanStack Store.
How It Works Internally
Async atoms work by:
- Starting in the
pending state when created
- Executing the async function immediately
- Using the reactive system to propagate state changes when the promise settles
- Automatically notifying subscribers when transitioning between states
The implementation leverages TanStack Store’s reactive core (based on alien-signals) to ensure efficient updates with minimal recomputations.
// Source: packages/store/src/atom.ts:88-124
export function createAsyncAtom<T>(
getValue: () => Promise<T>,
options?: AtomOptions<AsyncAtomState<T>>,
): ReadonlyAtom<AsyncAtomState<T>> {
const ref: { current?: InternalAtom<AsyncAtomState<T>> } = {}
const atom = createAtom<AsyncAtomState<T>>(() => {
getValue().then(
(data) => {
// Update to done state
const internalAtom = ref.current!
if (internalAtom._update({ status: 'done', data })) {
const subs = internalAtom.subs
if (subs !== undefined) {
propagate(subs)
shallowPropagate(subs)
flush()
}
}
},
(error) => {
// Update to error state
// ...
},
)
return { status: 'pending' }
}, options)
ref.current = atom as unknown as InternalAtom<AsyncAtomState<T>>
return atom
}
Next Steps