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.