Async Resources
Reactive data fetching with automatic loading states, caching, and error handling.
Resource Creation
Function-Based Resources
Create resources using async functions that return data, then trigger using the request
method.
// Create a resource with async fetcher
const todos = resource(() =>
fetch('/api/todos').then(r => r.json())
);
// Manually trigger the initial fetch
todos.request();
// Access state
console.log(todos.loading()); // true
console.log(todos.status()); // 'loading'
// After completion:
// todos.data() -> [{ id: 1, text: "Learn resources", completed: false }]
// todos.status() -> 'success'
// Refetch after mutations
const addTodo = async (text) => {
await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ text }),
headers: { 'Content-Type': 'application/json' }
});
todos.request(); // Refetch updated data
};
URL-Based Resources
Resources can be created directly from URLs for simple GET requests.
// Simple URL-based resource
const users = resource('/api/users'); // Automatically uses fetch + .json()
// Trigger fetch
users.request();
// Access data
console.log(users.loading()); // true initially
console.log(users.data()); // undefined initially
// After completion:
// users.data() -> [{ id: 1, name: "John" }, { id: 2, name: "Jane" }]
// users.status() -> 'success'
Reactive Keys
Use the key
option to create resources that refetch when dependencies change.
// Resource with reactive key (manual mode)
const userId = signal(1);
const posts = resource(
id => fetch(`/api/users/${id}/posts`).then(r => r.json()),
{ key: () => userId() }
);
posts.request();
// When key changes, invalidate to refetch
userId(2);
posts.invalidate(); // Refetch with new userId
// Access the updated data
console.log(posts.loading()); // true during refetch
console.log(posts.data()); // Previous data still available
// After completion:
// posts.data() -> [{ id: 1, title: "Post by User 2", body: "..." }]
// posts.status() -> 'success'
Auto-Fetch Mode
Set auto: true
to automatically refetch when key dependencies change, eliminating manual invalidate()
calls.
// Resource with auto-fetch enabled
const userId = signal(1);
const posts = resource(
id => fetch(`/api/users/${id}/posts`).then(r => r.json()),
{
key: () => userId(),
auto: true // Automatically refetch when userId changes
}
);
// Initial fetch happens automatically when effect runs
// No need to call posts.request()
// Changing userId automatically triggers refetch
userId(2); // Automatically fetches posts for user 2
userId(3); // Automatically fetches posts for user 3
// Access state during automatic transitions
console.log(posts.loading()); // true during auto-refetch
console.log(posts.data()); // Previous data available during transition
console.log(posts.status()); // 'loading' -> 'success'
Resource Mutations
Resources include a built-in mutation system for data modifications with lifecycle hooks, optimistic updates, and error handling.
Basic Mutations
Use the mutate
method to perform data modifications with full reactive state management.
// Resource for mutation operations
const createUserResource = resource(async (userData) => {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
});
// Perform mutation
const handleSubmit = async (formData) => {
try {
// Trigger mutation and get result
const newUser = await createUserResource.mutate(formData);
console.log('User created:', newUser);
// Check state during mutation
console.log(createUserResource.loading()); // true during request
console.log(createUserResource.status()); // 'loading'
} catch (error) {
console.error('Failed to create user:', error);
console.log(createUserResource.error()); // Error details
console.log(createUserResource.status()); // 'error'
}
};
// Example usage
handleSubmit({ name: 'John Doe', email: 'john@example.com' });
Mutation Lifecycle Hooks
Control mutation behavior with lifecycle hooks for optimistic updates and cleanup.
// Base resource for fetching todos
const todosResource = resource(() =>
fetch('/api/todos').then(r => r.json())
);
// Mutation resource with lifecycle hooks
const addTodoResource = resource(
async (todoData) => {
const response = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(todoData)
});
return response.json();
},
{
// Called before mutation starts
onMutate: async (variables) => {
// Optimistic update - add todo immediately
const tempTodo = {
id: Date.now(),
...variables,
pending: true
};
todosResource.setData(todos => [...(todos || []), tempTodo]);
// Return context for other hooks
return { tempTodo };
},
// Called when mutation succeeds
onSuccess: (newTodo, variables, context) => {
// Replace temporary todo with real one from server
todosResource.setData(todos =>
todos.map(todo =>
todo.id === context.tempTodo.id ? newTodo : todo
)
);
},
// Called when mutation fails
onError: (error, variables, context) => {
// Remove optimistic update on failure
todosResource.setData(todos =>
todos.filter(todo => todo.id !== context.tempTodo.id)
);
console.error('Failed to add todo:', error);
},
// Called after mutation completes (success or failure)
onSettled: async (data, error, variables, context) => {
// Refresh the list to ensure consistency
todosResource.invalidate();
}
}
);
// Usage
const handleAddTodo = (text) => {
addTodoResource.mutate({ text, completed: false });
};
// Check states
console.log(addTodoResource.loading()); // true during mutation
console.log(todosResource.data()); // Updated optimistically
Mutation Error Handling
Handle different types of mutation errors with structured error information.
// Mutation resource with error handling
const updateProfileResource = resource(
async (profileData) => {
const response = await fetch('/api/profile', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(profileData)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
},
{
onError: (error) => {
// Handle different error types
if (error.category === 'client') {
console.log('Please check your input and try again');
} else if (error.category === 'server') {
console.log('Server error, please try again later');
} else if (error.category === 'network') {
console.log('Network error, check your connection');
} else {
console.log(`Update failed: ${error.message}`);
}
}
}
);
// Perform mutation with error handling
const updateProfile = async (profileData) => {
try {
const result = await updateProfileResource.mutate(profileData);
console.log('Profile updated successfully:', result);
} catch (error) {
// Error already handled by onError hook
console.log('Update failed - error already logged');
// Access error details
console.log(updateProfileResource.error()); // Structured error info
console.log(updateProfileResource.status()); // 'error'
}
};
// Example usage
updateProfile({
name: 'John Doe',
email: 'john.doe@example.com'
});
Resource Configuration
Configure resources with initial data, caching strategies, and lifecycle callbacks for enhanced user experience.
// Resource with comprehensive configuration
const userId = signal(1);
const user = resource(
id => fetch(`/api/users/${id}`).then(r => r.json()),
{
key: () => userId(),
auto: true, // Automatically refetch when userId changes
initialData: { name: 'Loading...', email: '' }, // Shown immediately
cacheTime: 5 * 60 * 1000, // Cache for 5 minutes
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
}
}
);
// With auto: true, no need to call user.request()
// Resource automatically fetches when effect runs
// Access configured data and state
console.log(user.data()); // { name: 'Loading...', email: '' } initially
console.log(user.status()); // 'idle' -> 'loading' -> 'success'
// After successful load:
// user.data() -> { id: 1, name: 'John Doe', email: 'john@example.com' }
// Switch users - automatically refetches with auto: true
userId(2); // No need to call invalidate() with auto mode
Cache Control
Resources provide three caching strategies to optimize performance and control data freshness.
// Resource with caching enabled
const data = resource(
() => fetch('/api/expensive-data').then(r => r.json()),
{ cacheTime: 5 * 60 * 1000 } // Cache for 5 minutes
);
// Cache-first strategy - uses cached data when available
data.get(); // Checks cache first, fetches if expired/missing
// Force fresh strategy - always fetches new data
data.request(); // Bypasses cache, always makes network request
// Clear and refresh strategy - removes cache then fetches
data.invalidate(); // Clears cache entry and triggers fresh fetch
// Cache behavior examples
data.get(); // First call: fetches from network
data.get(); // Within 5 minutes: returns cached data instantly
data.request(); // Always fetches fresh data regardless of cache
// After 5 minutes cache expires:
data.get(); // Fetches fresh data and updates cache
Conditional Fetching
Control when resources should fetch data using the enabled
option.
// Conditional resource with enabled option
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
}
);
// Control when fetching is enabled
const loginUser = (id) => {
userType('authenticated');
userId(id);
userData.request(); // Manually trigger since enabled changed
};
const logoutUser = () => {
userType('guest');
userId(null);
userData.abort(); // Cancel any pending requests
userData.reset(); // Reset to initial state
};
// Check state based on conditions
console.log(userType()); // 'guest' initially
console.log(userData.status()); // 'idle' - won't fetch when disabled
loginUser(123);
console.log(userData.loading()); // true - now enabled and fetching
Timeout and Abort Control
Control request timing and cancellation with timeout settings and external abort signals.
// Resource with timeout configuration
const slowResource = resource(
() => new Promise(resolve =>
setTimeout(() => resolve({ data: 'Slow response' }), 10000)
),
{
timeout: 5000, // Abort after 5 seconds
initialData: { data: 'Initial' },
onError: (error) => {
if (error.category === 'abort') {
console.log('Request timed out after 5 seconds');
}
}
}
);
// Start request that will timeout
slowResource.request();
console.log(slowResource.data()); // { data: 'Initial' }
console.log(slowResource.loading()); // true
// After 5 seconds:
// slowResource.loading() -> false
// slowResource.error() -> ResourceError with category 'abort'
// slowResource.status() -> 'error'
You can also chain external abort signals for coordinated cancellation across multiple resources.
// External abort controller for coordinated cancellation
const controller = new AbortController();
const abortableResource = resource(
() => fetch('/api/long-operation').then(r => r.json()),
{
abortSignal: controller.signal,
onError: (error) => {
if (error.category === 'abort') {
console.log('Request cancelled externally');
}
}
}
);
// Start request
abortableResource.request();
console.log(abortableResource.loading()); // true
// Cancel externally - affects all resources using this controller
controller.abort();
// Check state after abort
console.log(abortableResource.loading()); // false
console.log(abortableResource.error()); // AbortError
console.log(abortableResource.status()); // 'error'
Manual State Management
Use setData
and reset
methods for direct state control and optimistic updates.
// Manual state management with resources
const counterResource = resource(
() => fetch('/api/counter').then(r => r.json()),
{ initialData: { count: 0 } }
);
// Direct state manipulation
counterResource.setData(data => ({
count: (data?.count || 0) + 1
}));
console.log(counterResource.data()); // { count: 1 }
// Optimistic updates with server sync
const optimisticIncrement = () => {
// Update UI immediately
const currentCount = counterResource.data()?.count || 0;
counterResource.setData({ count: currentCount + 1 });
// Then sync with server
fetch('/api/counter/increment', { method: 'POST' })
.then(() => counterResource.invalidate()) // Refresh from server
.catch(() => {
// Revert on error
counterResource.setData({ count: currentCount });
});
};
// Reset to initial state
counterResource.reset();
console.log(counterResource.data()); // { count: 0 }
console.log(counterResource.status()); // 'idle'
// Manual cache control
counterResource.get(); // Sync with server if needed
Request Deduplication
Resources automatically deduplicate identical concurrent requests to reduce network load and improve performance.
Deduplication Configuration
Use the deduplicate
option to control whether resources share concurrent requests with the same cache key.
// Deduplication enabled (default)
const publicDataResource = resource(
() => fetch('/api/public-data').then(r => r.json()),
{ deduplicate: true } // Default behavior
);
// Deduplication disabled for sensitive operations
const authResource = resource(
() => fetch('/api/auth/verify').then(r => r.json()),
{ deduplicate: false } // Each call makes its own request
);
// Examples
authResource.get(); // Always makes request
authResource.get(); // Makes another request (no deduplication)
publicDataResource.get(); // Makes request
publicDataResource.get(); // Uses shared request if concurrent
Deduplication Rules
Request deduplication follows specific rules:
- Same cache key + concurrent requests = shared network request
- Different cache keys = separate network requests
- Sequential requests with caching = cache-based deduplication
- Force refresh with
.request()
= bypasses deduplication
// Multiple resources with the same cache key share requests
const userId = signal(123);
const userProfileResource = resource(
id => fetch(`/api/users/${id}`).then(r => r.json()),
{ key: () => userId() }
);
const userSettingsResource = resource(
id => fetch(`/api/users/${id}`).then(r => r.json()),
{ key: () => userId() }
);
// Concurrent calls share one network request
userProfileResource.get();
userSettingsResource.get(); // Shares request with userProfileResource
// Both resources receive the same data
console.log(userProfileResource.data() === userSettingsResource.data()); // true
Advanced Usage Patterns
Request Lifecycle Management
Resources provide fine-grained control over request lifecycle through dedicated methods.
// Resource with comprehensive lifecycle control
const data = resource(
() => new Promise(resolve =>
setTimeout(() => resolve({ message: 'Slow data loaded!' }), 3000)
),
{ initialData: { message: 'Initial state' } }
);
// Available lifecycle methods
data.request(); // Start fresh request
data.abort(); // Cancel ongoing request
data.get(); // Cache-aware fetch
data.invalidate(); // Clear cache and refetch
data.reset(); // Reset to initial state
// Check state at any time
console.log(data.status()); // 'idle' | 'loading' | 'success' | 'error'
console.log(data.loading()); // Boolean loading state
console.log(data.data()); // Current data
console.log(data.error()); // Error information if any
Error Handling Strategies
Implement robust error handling with graceful degradation.
```jsx
// Resource with robust error handling
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) => {
console.error('Resource fetch failed:', error);
}
}
);
// Retry with exponential backoff
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
console.log(`Retrying in ${delay}ms (attempt ${attempts})`);
setTimeout(attempt, delay);
}
}, 100);
};
attempt();
};
// Usage
data.request(); // Initial attempt
console.log(data.loading()); // true
// If it fails, retry with backoff
if (data.error()) {
retryWithExponentialBackoff();
}
Resource Composition Patterns
Compose multiple resources for complex data flows.
// Multiple resources with orchestrated data flow
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.get(); // Use cache-aware get for categories
// Trigger dependent resource when user loads
const checkUserPosts = () => {
if (user.data() && user.status() === 'success') {
userPosts.request();
}
};
// Change user and coordinate updates
const switchUser = (newId) => {
userId(newId);
user.invalidate();
userPosts.abort(); // Cancel any pending posts request
};
// Check composed state
console.log(user.data()); // User data when loaded
console.log(userPosts.data()); // Posts when loaded
console.log(categories.data()); // Cached categories
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
Resource State Flow
Resources follow a predictable state flow that coordinates async operations:
idle → loading → success/error → idle (on reset)
↖ ↓ ↙
abort() invalidate()
Cache Integration: Resources check cache before network requests, automatically managing data freshness and performance optimization.
Request Deduplication: Multiple resources requesting the same data share a single network call, reducing server load and improving response times.
Memory Management: Automatic cleanup of expired cache entries and proper disposal of reactive dependencies prevents memory leaks in long-running applications.