Async Resources
Resources provide a reactive abstraction for managing asynchronous data. They automatically handle loading states, error conditions, caching strategies, and request lifecycle management, eliminating the need for manual state coordination.
Core Resource Patterns
Async operations with reactive state, automatically coordinating loading, errors, and data availability across application components.
const UserProfile = () => {
const userId = signal(1);
const user = resource(
id => fetch(`/api/users/${id}`).then(r => r.json()),
{ key: () => userId() }
);
// Trigger initial fetch
user.request();
return (
<div>
<select value={userId} onChange={e => userId(+e.target.value)}>
<option value={1}>User 1</option>
<option value={2}>User 2</option>
</select>
{user.loading() && <div>Loading user...</div>}
{user.error() && <div>Error: {user.error()}</div>}
{user.data() && (
<div>
<h1>{user.data().name}</h1>
<p>{user.data().email}</p>
<p>Status: {user.status()}.</p>
</div>
)}
<button onClick={() => user.invalidate()}>Refresh</button>
</div>
);
};
Resource Creation
Function-Based Resources
Create resources using async functions that return data, then trigger using the request
method.
const TodoApp = () => {
const todos = resource(() =>
fetch('/api/todos').then(r => r.json())
);
// Manually trigger the initial fetch
todos.request();
const addTodo = async (text) => {
await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ text }),
headers: { 'Content-Type': 'application/json' }
});
todos.request(); // Refetch after mutation
};
return (
<div>
<button onClick={() => addTodo('New todo')}>Add Todo</button>
{todos.loading() && <div>Loading todos...</div>}
{todos.error() && <div>Error loading todos</div>}
{todos.data() && (
<ul>
{forEach(todos.data, todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
)}
</div>
);
};
URL-Based Resources
Resources can be created directly from URLs for simple GET requests.
const UserList = () => {
const users = resource('/api/users'); // Automatically uses fetch + .json()
users.request(); // Trigger fetch
return (
<div>
{users.loading() && <div>Loading users...</div>}
{users.data() && (
<ul>
{forEach(users.data, user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)}
</div>
);
};
Reactive Keys
Use the key
option to create resources that refetch when dependencies change.
When combined with invalidate
, this enables automatic data synchronization.
const UserPosts = () => {
const userId = signal(1);
const posts = resource(
id => fetch(`/api/users/${id}/posts`).then(r => r.json()),
{ key: () => userId() }
);
posts.request();
return (
<div>
<select value={userId} onChange={e => {
userId(+e.target.value);
posts.invalidate(); // Refetch with new userId
}}>
<option value={1}>User 1</option>
<option value={2}>User 2</option>
</select>
{posts.loading() && <div>Loading posts...</div>}
{posts.data() && (
<div>
<h2>Posts by User {userId}.</h2>
{forEach(posts.data, post => (
<article key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</article>
))}
</div>
)}
</div>
);
};
Resource Configuration
Initial Data and Lifecycle Hooks
Configure resources with initial data and lifecycle callbacks for enhanced user experience.
const UserProfile = () => {
const userId = signal(1);
const user = resource(
id => fetch(`/api/users/${id}`).then(r => r.json()),
{
key: () => userId(),
initialData: { name: 'Loading...', email: '' }, // Shown immediately
onSuccess: (userData) => {
console.log('User loaded:', userData.name);
// Could trigger analytics, notifications, etc.
},
onError: (error) => {
console.error('Failed to load user:', error);
// Could trigger error reporting
}
}
);
user.request();
return (
<div>
<div class="user-card">
<h1>{user.data().name}</h1>
<p>{user.data().email}</p>
{user.loading() && <div class="loading-spinner">Updating...</div>}
{user.error() && (
<div class="error-message">
Failed to load user data
</div>
)}
<p>Status: <span class={`status-${user.status()}`}>{user.status()}.</span></p>
</div>
<button onClick={() => {
userId(userId() === 1 ? 2 : 1);
user.invalidate();
}}>
Switch User
</button>
</div>
);
};
Caching Strategy
Use fetch()
to leverage cache, or request()
to force fresh data.
const CachedDataDemo = () => {
const data = resource(
() => fetch('/api/expensive-data').then(r => r.json()),
{ cacheTime: 5 * 60 * 1000 } // Cache for 5 minutes
);
return (
<div>
<h2>Cached Data Example</h2>
<div class="controls">
<button onClick={() => data.fetch()}>
Load (use cache if available)
</button>
<button onClick={() => data.request()}>
Force Refresh (bypass cache)
</button>
<button onClick={() => data.invalidate()}>
Clear Cache & Refresh
</button>
</div>
{data.loading() && <div>Loading...</div>}
{data.data() && (
<div>
<h3>Data loaded at: {new Date().toLocaleTimeString()}</h3>
<pre>{JSON.stringify(data.data(), null, 2)}</pre>
</div>
)}
</div>
);
};
Conditional Fetching
Control when resources should fetch data using the enabled
option.
const ConditionalData = () => {
const userType = signal('guest');
const userId = signal(null);
const userData = resource(
id => fetch(`/api/users/${id}`).then(r => r.json()),
{
key: () => userId(),
enabled: userType() === 'authenticated' && !!userId() // Only fetch when conditions met
}
);
const loginUser = (id) => {
userType('authenticated');
userId(id);
userData.request(); // Manually trigger since enabled changed
};
return (
<div>
<div class="auth-controls">
<button onClick={() => loginUser(1)}>Login as User 1</button>
<button onClick={() => loginUser(2)}>Login as User 2</button>
<button onClick={() => {
userType('guest');
userId(null);
userData.abort();
}}>Logout</button>
</div>
<div class="user-status">
<p>Status: {userType}.</p>
{userType() === 'authenticated' && (
<>
{userData.loading() && <p>Loading user data...</p>}
{userData.data() && (
<div>
<h3>Welcome, {userData.data().name}!</h3>
<p>Email: {userData.data().email}</p>
</div>
)}
</>
)}
</div>
</div>
);
};
Advanced Usage Patterns
Request Lifecycle Management
Resources provide fine-grained control over request lifecycle through dedicated methods.
const RequestLifecycleDemo = () => {
const data = resource(
() => new Promise(resolve =>
setTimeout(() => resolve({ message: 'Slow data loaded!' }), 3000)
),
{ initialData: { message: 'Initial state' } }
);
return (
<div>
<h2>Resource Lifecycle Controls</h2>
<div class="controls">
<button onClick={() => data.request()}>Start Request</button>
<button onClick={() => data.abort()}>Abort Request</button>
<button onClick={() => data.fetch()}>Fetch (Cache-Aware)</button>
<button onClick={() => data.invalidate()}>Invalidate & Refetch</button>
</div>
<div class="status">
<p>Status: <strong>{data.status()}</strong></p>
<p>Loading: <strong>{data.loading() ? 'Yes' : 'No'}</strong></p>
<p>Has Error: <strong>{data.error() ? 'Yes' : 'No'}</strong></p>
</div>
<div class="data">
<h3>Current Data:</h3>
<p>{data.data().message}</p>
</div>
{data.error() && (
<div class="error">
<h3>Error Details:</h3>
<pre>{String(data.error())}</pre>
</div>
)}
</div>
);
};
Error Handling Strategies
Implement robust error handling with graceful degradation.
const RobustErrorHandling = () => {
const data = resource(
() => fetch('/api/unreliable-endpoint').then(async response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}),
{
initialData: null,
onError: (error) => {
// Log error for debugging/monitoring
console.error('Resource fetch failed:', error);
}
}
);
const retryWithExponentialBackoff = () => {
let attempts = 0;
const maxAttempts = 3;
const attempt = () => {
data.request();
// Check if request succeeded after delay
setTimeout(() => {
if (data.error() && attempts < maxAttempts) {
attempts++;
const delay = Math.pow(2, attempts) * 1000; // 2s, 4s, 8s
setTimeout(attempt, delay);
}
}, 100);
};
attempt();
};
return (
<div>
<h2>Robust Error Handling</h2>
<button onClick={() => data.request()}>Load Data</button>
<button onClick={retryWithExponentialBackoff}>Retry with Backoff</button>
{data.loading() && (
<div class="loading">
<p>Loading data...</p>
<button onClick={() => data.abort()}>Cancel</button>
</div>
)}
{data.error() && (
<div class="error-state">
<h3>Something went wrong</h3>
<p>We couldn't load the data you requested.</p>
<details>
<summary>Error details</summary>
<pre>{String(data.error())}</pre>
</details>
<button onClick={() => data.request()}>Try Again</button>
</div>
)}
{data.data() && data.status() === 'success' && (
<div class="success-state">
<h3>Data loaded successfully!</h3>
<pre>{JSON.stringify(data.data(), null, 2)}</pre>
</div>
)}
</div>
);
};
Resource Composition Patterns
Compose multiple resources for complex data flows.
const ResourceComposition = () => {
const userId = signal(1);
// Primary resource
const user = resource(
id => fetch(`/api/users/${id}`).then(r => r.json()),
{
key: () => userId(),
onSuccess: (userData) => console.log('User loaded:', userData.name)
}
);
// Dependent resource - only fetch when user is available
const userPosts = resource(
id => fetch(`/api/users/${id}/posts`).then(r => r.json()),
{
key: () => userId(),
enabled: !!user.data() && user.status() === 'success'
}
);
// Independent resource with caching
const categories = resource(
() => fetch('/api/categories').then(r => r.json()),
{ cacheTime: 10 * 60 * 1000 } // Cache categories for 10 minutes
);
// Initialize data loading
user.request();
categories.fetch(); // Use cache-aware fetch for categories
// Trigger dependent resource when user loads
const checkUserPosts = () => {
if (user.data() && user.status() === 'success') {
userPosts.request();
}
};
return (
<div>
<h2>Resource Composition</h2>
<select value={userId} onChange={e => {
const newId = +e.target.value;
userId(newId);
user.invalidate();
userPosts.abort(); // Cancel any pending posts request
}}>
<option value={1}>User 1</option>
<option value={2}>User 2</option>
<option value={3}>User 3</option>
</select>
<div class="user-section">
<h3>User Information</h3>
{user.loading() && <p>Loading user...</p>}
{user.error() && <p>Failed to load user</p>}
{user.data() && (
<div>
<p>Name: {user.data().name}</p>
<button onClick={checkUserPosts}>Load Posts</button>
</div>
)}
</div>
<div class="posts-section">
<h3>User Posts</h3>
{userPosts.loading() && <p>Loading posts...</p>}
{userPosts.error() && <p>Failed to load posts</p>}
{userPosts.data() && (
<ul>
{forEach(userPosts.data, post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)}
</div>
<div class="categories-section">
<h3>Categories (Cached)</h3>
{categories.loading() && <p>Loading categories...</p>}
{categories.data() && (
<ul>
{forEach(categories.data, cat => (
<li key={cat.id}>{cat.name}</li>
))}
</ul>
)}
</div>
</div>
);
};
Internal Mechanisms
Resources coordinate reactive primitives, caching systems, and async operations to provide predictable data management patterns.
Resource State Machine
Resources maintain a simple state machine with four states.
• idle - No active request
• loading - Request in progress
• success - Data available
• error - Request failed
idle → loading → success
↖ ↓ ↙
abort() error
Reactive Foundation
Resources use four internal reactive signals.
• data - Current fetched data or initial value
• error - Error information when requests fail
• loading - Boolean flag for active requests
• status - Computed state reflecting current position in state machine
UI components automatically update when any signal changes, ensuring consistent reactive behavior.
Caching System
Time-based cache minimizes redundant requests.
• Global Map stores data with timestamps
• Cache Lookup checks for valid data before requests
• Time Validation compares age against configured cache time
• Key Isolation separates different data sets
Request Lifecycle
Sophisticated coordination handles race conditions.
• Abort Protection prevents updates from cancelled requests
• Dependency Management avoids infinite reactive loops
• Async Coordination synchronizes loading states with data updates
• Cleanup Handling prevents memory leaks and unwanted updates
Method Behaviors
Each method serves specific lifecycle purposes.
• request() - Forces fresh fetch, bypassing cache
• fetch() - Uses cache when available, fetches on miss
• abort() - Cancels requests and resets to initial state
• invalidate() - Clears cache and triggers fresh fetch
Memory Efficiency
Several strategies optimize performance.
• Shared Cache reduces memory usage across instances
• Signal Cleanup properly disposes computed values
• Reference Management prevents circular dependencies
• Async Cleanup handles component unmount scenarios