Skip to main content
This example demonstrates how to manage asynchronous operations, loading states, and error handling in Stan.js. Based on the real examples from the Stan.js repository.

Live Demo

Try it on StackBlitz

See the async data example in action

Store Setup

Create a store that tracks users, loading states, and errors:
store.ts
import { createStore } from 'stan-js'

type User = {
    id: string
    name: string
    email: string
}

export const { useStore, getState, actions } = createStore({
    users: [] as User[],
    isLoading: false,
    error: null as string | null,
    // Computed value for user count
    get userCount() {
        return this.users.length
    },
})

Async Data Fetching

Create async functions that update the store:
api.ts
import { actions } from './store'

export const fetchUsers = async () => {
    // Set loading state
    actions.setIsLoading(true)
    actions.setError(null)

    try {
        const response = await fetch('https://api.example.com/users')
        
        if (!response.ok) {
            throw new Error('Failed to fetch users')
        }

        const users = await response.json()
        
        // Update users
        actions.setUsers(users)
    } catch (error) {
        // Handle errors
        actions.setError(error instanceof Error ? error.message : 'Unknown error')
    } finally {
        // Always clear loading state
        actions.setIsLoading(false)
    }
}

export const fetchMoreUsers = async () => {
    actions.setIsLoading(true)
    actions.setError(null)

    try {
        const response = await fetch('https://randommer.io/Name', {
            headers: {
                'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
            },
            body: 'type=firstname&number=20',
            method: 'POST',
        })

        const newUsers = await response.json() as string[]
        
        // Append to existing users
        actions.setUsers(prev => [
            ...prev,
            ...newUsers.map((name, index) => ({
                id: `${Date.now()}-${index}`,
                name,
                email: `${name.toLowerCase()}@example.com`,
            })),
        ])
    } catch (error) {
        actions.setError('Failed to load more users')
    } finally {
        actions.setIsLoading(false)
    }
}
You can call actions from anywhere - inside components, event handlers, or standalone functions. No need to wrap in hooks!

Component Implementation

UsersList.tsx
import { useEffect } from 'react'
import { useStore } from './store'
import { fetchUsers, fetchMoreUsers } from './api'

export const UsersList = () => {
const { users, isLoading, error, userCount } = useStore()

// Fetch on mount
useEffect(() => {
    fetchUsers()
}, [])

if (error) {
    return (
        <div>
            <p>Error: {error}</p>
            <button onClick={fetchUsers}>Retry</button>
        </div>
    )
}

return (
    <div>
        <h2>Users ({userCount})</h2>
        
        {isLoading && users.length === 0 ? (
            <p>Loading users...</p>
        ) : (
            <ul>
                {users.map(user => (
                    <li key={user.id}>
                        {user.name} - {user.email}
                    </li>
                ))}
            </ul>
        )}

        <button
            onClick={fetchMoreUsers}
            disabled={isLoading}
        >
            {isLoading ? 'Loading...' : 'Load More'}
        </button>
    </div>
)
}

Patterns & Best Practices

Loading State Patterns

// Show spinner on first load
if (isLoading && users.length === 0) {
    return <Spinner />
}

Error Handling Strategies

const { error } = useStore()

return (
    <div>
        {error && (
            <div className="error-banner">
                {error}
                <button onClick={() => actions.setError(null)}>×</button>
            </div>
        )}
        <UsersList />
    </div>
)
const { users, error, isLoading } = useStore()

if (error) {
    return <ErrorState error={error} onRetry={fetchUsers} />
}

if (isLoading) {
    return <LoadingState />
}

return <SuccessState users={users} />
import { toast } from 'sonner'
import { actions } from './store'

export const fetchUsers = async () => {
    try {
        const users = await api.getUsers()
        actions.setUsers(users)
        toast.success('Users loaded successfully')
    } catch (error) {
        toast.error('Failed to load users')
        actions.setError(error.message)
    }
}
const fetchWithRetry = async (retries = 3) => {
    for (let i = 0; i < retries; i++) {
        try {
            return await fetchUsers()
        } catch (error) {
            if (i === retries - 1) throw error
            await new Promise(r => setTimeout(r, 1000 * (i + 1)))
        }
    }
}

Updates Outside React

Stan.js works seamlessly with updates outside React components:
timer.ts
import { actions } from './store'

// Update state from setInterval
setInterval(() => {
    actions.setCurrentTime(new Date())
}, 1000)
CurrentTime.tsx
import { useStore } from './store'

const CurrentTime = () => {
    const { currentTime } = useStore()

    return <h2>{currentTime.toLocaleTimeString()}</h2>
}
This example is from the Stan.js React demo. The timer updates every second from outside React, and components reactively update!

Advanced Patterns

import { debounce } from 'lodash'
import { actions } from './store'

const searchUsers = debounce(async (query: string) => {
    actions.setIsLoading(true)
    try {
        const users = await api.searchUsers(query)
        actions.setUsers(users)
    } catch (error) {
        actions.setError('Search failed')
    } finally {
        actions.setIsLoading(false)
    }
}, 300)

export const SearchInput = () => (
    <input
        onChange={e => searchUsers(e.target.value)}
        placeholder="Search users..."
    />
)

Polling

import { useEffect } from 'react'
import { fetchUsers } from './api'

export const usePolling = (interval: number) => {
    useEffect(() => {
        const id = setInterval(fetchUsers, interval)
        return () => clearInterval(id)
    }, [interval])
}

// Usage
export const App = () => {
    usePolling(5000) // Poll every 5 seconds
    return <UsersList />
}

Request Cancellation

let controller: AbortController | null = null

export const fetchUsers = async () => {
    // Cancel previous request
    controller?.abort()
    controller = new AbortController()

    actions.setIsLoading(true)
    try {
        const response = await fetch('/api/users', {
            signal: controller.signal,
        })
        const users = await response.json()
        actions.setUsers(users)
    } catch (error) {
        if (error.name !== 'AbortError') {
            actions.setError('Failed to fetch')
        }
    } finally {
        actions.setIsLoading(false)
    }
}

Next Steps

React Native

Learn about mobile development with Stan.js

Basic Counter

Start with the fundamentals

Effects

React to state changes with effects

Actions API

Explore the actions API

Build docs developers (and LLMs) love