Async State
Most applications deal with asynchronous data. stroid makes it straightforward to model loading, success, and error states directly in your store.
The Async Pattern
The recommended pattern uses a status field that tracks the lifecycle of an async operation:
user-store.ts
import { createStore, setStore } from 'stroid'
type Status = 'idle' | 'loading' | 'success' | 'error'
createStore('user', {
user: null as User | null,
status: 'idle' as Status,
error: null as string | null,
})
export async function fetchUser(id: string) {
setStore('user', (s) => {
s.status = 'loading'
s.error = null
})
try {
const res = await fetch(`/api/users/${id}`)
if (!res.ok) throw new Error('Failed to fetch')
const data = await res.json()
setStore('user', (s) => {
s.user = data
s.status = 'success'
})
} catch (err) {
setStore('user', (s) => {
s.error = err instanceof Error ? err.message : 'Unknown error'
s.status = 'error'
})
}
}
export function resetUser() {
setStore('user', (s) => {
s.user = null
s.status = 'idle'
s.error = null
})
}Consuming Async State
UserProfile.tsx
import { useEffect } from 'react'
import { useStore } from 'stroid'
import { fetchUser } from './user-store'
function UserProfile({ id }: { id: string }) {
const status = useStore('user', (s) => s.status)
const user = useStore('user', (s) => s.user)
const error = useStore('user', (s) => s.error)
useEffect(() => {
fetchUser(id)
}, [id])
if (status === 'loading') return <Spinner />
if (status === 'error') return <Error message={error} />
if (!user) return null
return <div>{user.name}</div>
}Tip
Notice that each
await boundary triggers an update. Your component sees status: 'loading' immediately, then transitions to 'success' or 'error' when the request completes.Parallel Requests
parallel.ts
import { createStore, setStore } from 'stroid'
createStore('dashboard', { users: [] as User[], posts: [] as Post[], loading: false })
export async function fetchAll() {
setStore('dashboard', (s) => {
s.loading = true
})
const [users, posts] = await Promise.all([
fetch('/api/users').then((r) => r.json()),
fetch('/api/posts').then((r) => r.json()),
])
setStore('dashboard', (s) => {
s.users = users
s.posts = posts
s.loading = false
})
}Optimistic Updates
For instant UI feedback, update state optimistically and roll back on failure:
optimistic.ts
import { createStore, setStore } from 'stroid'
createStore('todos', { todos: [] as Todo[] })
export async function toggleTodo(id: number) {
let previousDone: boolean | null = null
let newDone: boolean | null = null
setStore('todos', (s) => {
const todo = s.todos.find((t) => t.id === id)
if (!todo) return
previousDone = todo.done
todo.done = !todo.done
newDone = todo.done
})
if (newDone === null) return
try {
await fetch(`/api/todos/${id}`, {
method: 'PATCH',
body: JSON.stringify({ done: newDone }),
})
} catch {
setStore('todos', (s) => {
const todo = s.todos.find((t) => t.id === id)
if (todo && previousDone !== null) todo.done = previousDone
})
}
}