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