asyncState()
Create an enhanced async state with automatic loading, error handling, and race condition prevention.
Quick Start (30 seconds)
// Create async state
const userState = asyncState(null);
// Execute async operation
await execute(userState, async (signal) => {
const response = await fetch('/api/user', { signal });
return response.json();
});
// Check results
console.log(userState.data); // { id: 1, name: 'Alice' }
console.log(userState.loading); // false
console.log(userState.isSuccess); // trueThe magic: asyncState() creates a smart container that automatically manages loading, errors, and data—no manual tracking needed!
What is asyncState()?
asyncState() is a factory function that creates a reactive state object specifically designed for async operations. It includes automatic state management, computed properties, and race condition prevention.
Key points:
- Creates reactive state with loading/error/data properties
- Includes computed properties (isSuccess, isError, isIdle)
- Automatic race condition prevention
- Optional callbacks for success/error
- Works with execute(), abort(), reset(), refetch()
Syntax
// Basic - no initial value
const state = asyncState();
// With initial value
const state = asyncState({ default: 'data' });
// With callbacks
const state = asyncState(null, {
onSuccess: (data) => console.log('Success!', data),
onError: (error) => console.error('Error!', error)
});
// Via ReactiveUtils
const state = asyncState(initialValue, options);Parameters:
initialValue(optional) - Initial data value (default:null)options(optional) - Configuration object
Options:
onSuccess- Callback when operation succeedsonError- Callback when operation fails
Returns:
- Reactive async state object
Why Does This Exist?
Without asyncState
// Manual async state management
const state = ReactiveUtils.state({
data: null,
loading: false,
error: null
});
async function fetchUser() {
state.loading = true;
state.error = null;
try {
const response = await fetch('/api/user');
state.data = await response.json();
} catch (error) {
state.error = error;
} finally {
state.loading = false;
}
}
// No race condition protection!
// Must manually track everything!Problems: Manual tracking, no race protection, boilerplate everywhere.
With asyncState
const userState = asyncState(null);
await execute(userState, async () => {
const response = await fetch('/api/user');
return response.json();
});
// Everything automatic!
// Race conditions prevented!
// Clean code!Solution: Automatic state management, built-in safety, minimal code.
Parameters
Parameter 1: initialValue
Type: Any
Default: null
Purpose: Initial value for the data property
Examples:
// Null (most common)
const state1 = asyncState(null);
console.log(state1.data); // null
console.log(state1.isIdle); // true
// Object
const state2 = asyncState({ user: 'Guest' });
console.log(state2.data); // { user: 'Guest' }
console.log(state2.isIdle); // false (has data)
// Array
const state3 = asyncState([]);
console.log(state3.data); // []
// Primitive
const state4 = asyncState(0);
console.log(state4.data); // 0Parameter 2: options
Type: Object (optional)
Properties:
onSuccess: Function called when operation succeedsonError: Function called when operation fails
Examples:
const state = asyncState(null, {
onSuccess: (data) => {
console.log('✓ Loaded:', data);
showNotification('Data loaded successfully');
},
onError: (error) => {
console.error('✗ Failed:', error.message);
showNotification('Failed to load data');
}
});
await execute(state, async () => {
const response = await fetch('/api/data');
return response.json();
});
// Logs: ✓ Loaded: { ... }Return Value
Returns a reactive async state object with:
Properties
const state = asyncState(null);
// Data properties
state.data // Result data (any)
state.loading // Loading state (boolean)
state.error // Error object (Error | null)
state.requestId // Request counter (number)
state.abortController // AbortController (AbortController | null)
// Computed properties
state.isSuccess // Has data, no error (boolean, read-only)
state.isError // Has error, not loading (boolean, read-only)
state.isIdle // No data, no error, not loading (boolean, read-only)Usage with Namespace Functions
// Execute async operation
execute(state, asyncFn)
// Abort current operation
abort(state)
// Reset to initial state
reset(state)
// Re-run last operation
refetch(state)
// Clean up reactivity
cleanup(state)Basic Usage
Example 1: Simple Fetch
const postsState = asyncState(null);
async function loadPosts() {
await execute(postsState, async () => {
const response = await fetch('/api/posts');
return response.json();
});
}
await loadPosts();
if (postsState.isSuccess) {
console.log('Posts:', postsState.data);
}Example 2: With Initial Value
const configState = asyncState({
theme: 'light',
language: 'en'
});
console.log('Default:', configState.data);
// { theme: 'light', language: 'en' }
await execute(configState, async () => {
const response = await fetch('/api/config');
return response.json();
});
console.log('Loaded:', configState.data);
// Server config replaces defaultsExample 3: With Callbacks
const dataState = asyncState(null, {
onSuccess: (data) => {
console.log('Data loaded!');
updateUI(data);
logEvent('data_loaded');
},
onError: (error) => {
console.error('Load failed:', error.message);
showErrorDialog(error.message);
logEvent('data_error', { error: error.message });
}
});
await execute(dataState, async () => {
const response = await fetch('/api/data');
return response.json();
});
// Callbacks fire automaticallyCommon Patterns
Pattern 1: API Service
class UserService {
constructor() {
this.userState = asyncState(null, {
onSuccess: (user) => {
console.log('User loaded:', user.name);
},
onError: (error) => {
console.error('Failed to load user:', error);
}
});
}
async loadUser(id) {
await execute(this.userState, async (signal) => {
const response = await fetch(`/api/users/${id}`, { signal });
return response.json();
});
return this.userState.data;
}
get user() {
return this.userState.data;
}
get loading() {
return this.userState.loading;
}
}
const service = new UserService();
await service.loadUser(123);
console.log(service.user);Pattern 2: Component State
class DataComponent {
constructor() {
this.state = asyncState(null, {
onSuccess: () => this.render(),
onError: () => this.renderError()
});
}
async mount() {
await execute(this.state, async () => {
const response = await fetch('/api/data');
return response.json();
});
}
render() {
if (this.state.loading) {
return '<div>Loading...</div>';
}
if (this.state.isSuccess) {
return `<div>Data: ${this.state.data}</div>`;
}
}
renderError() {
return `<div>Error: ${this.state.error.message}</div>`;
}
destroy() {
abort(this.state);
cleanup(this.state);
}
}Pattern 3: Multiple States
const states = {
users: asyncState(null),
posts: asyncState(null),
comments: asyncState(null)
};
async function loadAll() {
await Promise.all([
execute(states.users, () =>
fetch('/api/users').then(r => r.json())
),
execute(states.posts, () =>
fetch('/api/posts').then(r => r.json())
),
execute(states.comments, () =>
fetch('/api/comments').then(r => r.json())
)
]);
}
const allLoaded = computed(() => {
return states.users.isSuccess &&
states.posts.isSuccess &&
states.comments.isSuccess;
});Pattern 4: Paginated Data
function createPaginatedState() {
return asyncState({
items: [],
page: 1,
totalPages: 1,
hasMore: false
}, {
onSuccess: (data) => {
console.log(`Loaded page ${data.page} of ${data.totalPages}`);
}
});
}
const paginatedState = createPaginatedState();
async function loadPage(page) {
await execute(paginatedState, async () => {
const response = await fetch(`/api/items?page=${page}`);
const data = await response.json();
return {
items: data.items,
page: data.page,
totalPages: data.totalPages,
hasMore: data.page < data.totalPages
};
});
}
await loadPage(1);
console.log(paginatedState.data.items.length);Pattern 5: Cached State
const cache = new Map();
function createCachedState(key) {
const state = asyncState(null);
// Check cache first
if (cache.has(key)) {
state.data = cache.get(key);
}
return state;
}
async function loadWithCache(key, url) {
const state = createCachedState(key);
if (!state.data) {
await execute(state, async () => {
const response = await fetch(url);
const data = await response.json();
cache.set(key, data);
return data;
});
}
return state.data;
}
const data = await loadWithCache('users', '/api/users');Advanced Usage
Custom State Shape
function createCustomAsyncState(initialData) {
const state = asyncState({
data: initialData,
metadata: {
fetchedAt: null,
fetchCount: 0
}
});
// Wrap execute to track metadata
const originalExecute = execute;
const customExecute = async (fn) => {
await originalExecute(state, async (signal) => {
const result = await fn(signal);
return {
data: result,
metadata: {
fetchedAt: Date.now(),
fetchCount: state.data.metadata.fetchCount + 1
}
};
});
};
return { state, execute: customExecute };
}State with Validation
function createValidatedState(validator) {
return asyncState(null, {
onSuccess: (data) => {
const errors = validator(data);
if (errors.length > 0) {
console.warn('Data validation warnings:', errors);
}
}
});
}
const userState = createValidatedState((user) => {
const errors = [];
if (!user.email) errors.push('Missing email');
if (!user.name) errors.push('Missing name');
return errors;
});Edge Cases
Gotcha 1: Callbacks Fire Once Per Execute
let successCount = 0;
const state = asyncState(null, {
onSuccess: () => {
successCount++;
console.log('Success count:', successCount);
}
});
await execute(state, async () => 'data 1');
// Logs: Success count: 1
await execute(state, async () => 'data 2');
// Logs: Success count: 2
// Each execute fires callback onceGotcha 2: Initial Value Doesn't Trigger Callbacks
const state = asyncState({ initial: 'data' }, {
onSuccess: (data) => {
console.log('Success:', data);
}
});
// No log (initial value doesn't trigger callback)
await execute(state, async () => ({ loaded: 'data' }));
// Logs: Success: { loaded: 'data' }Gotcha 3: Callbacks Not Called on Abort
const state = asyncState(null, {
onSuccess: () => console.log('Success'),
onError: () => console.log('Error')
});
execute(state, async () => {
await new Promise(r => setTimeout(r, 5000));
return 'data';
});
setTimeout(() => abort(state), 1000);
// Neither callback fires (abort is not error or success)When to Use
Use asyncState() for:
- ✅ API calls and data fetching
- ✅ Form submissions
- ✅ File uploads
- ✅ Any async operation needing state tracking
- ✅ Operations requiring race condition prevention
Don't use for:
- ❌ Synchronous state (use regular
state()) - ❌ Simple boolean flags (use
state()) - ❌ Non-async operations
Summary
What it is: Factory function creating enhanced async state
Parameters:
initialValue- Initial data (default:null)options- Callbacks and config
Returns: Reactive async state object with properties and computed values
Use with: execute(), abort(), reset(), refetch()
Features: Automatic loading/error management, race condition prevention, computed states
Quick Reference
// Create async state
const state = asyncState(initialValue, {
onSuccess: (data) => { /* ... */ },
onError: (error) => { /* ... */ }
});
// Properties
state.data // Result data
state.loading // Loading state
state.error // Error object
state.requestId // Request counter
state.isSuccess // Success computed
state.isError // Error computed
state.isIdle // Idle computed
// Usage
await execute(state, asyncFn);
abort(state);
reset(state);
await refetch(state);
cleanup(state);One-Line Rule: Use asyncState() to create a smart async container that automatically manages loading, errors, and data—with built-in race condition prevention.