All files / src/primitives resource.ts

100% Statements 43/43
100% Branches 11/11
100% Functions 2/2
100% Lines 43/43

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 1091x 1x 1x 1x                                                                                                         1x 5x 5x 5x 5x 5x 5x 5x   5x 7x 7x 7x   7x     7x 5x 7x   7x 7x 7x 7x 5x 5x 7x 1x 1x 1x 7x 7x 6x 6x 7x 7x   7x 5x     5x       5x   5x   5x 5x  
import { createSignal } from './signal';
import { createEffect } from './effect';
import { createMemo } from './memo';
import { onCleanup } from '../lifecycle/lifecycle';
import type { Resource, SignalGetter, ReactiveOptions } from '../types';
 
/**
 * Information passed to the fetcher function, including an AbortSignal
 * to cancel the request if the resource is disposed or re-fetched.
 */
export interface FetcherInfo {
  signal: AbortSignal;
}
 
type Fetcher<S, T> = (source: S, info?: FetcherInfo) => T | Promise<T>;
 
/**
 * Creates a resource that handles asynchronous data fetching with reactive state management.
 *
 * Resources automatically track loading, error, and data states. They re-fetch data
 * whenever their source signal changes and provide reactive access to all states.
 *
 * @param source A signal that provides the input for the fetcher function
 * @param fetcher A function that takes the source value and returns data or a Promise
 * @param options Optional configuration including debug name
 * @returns A resource object with reactive loading, error, and data states
 *
 * @example
 * ```typescript
 * const [userId, setUserId] = createSignal(1);
 *
 * const userResource = createResource(userId, async (id) => {
 *   const response = await fetch(`/api/users/${id}`);
 *   return response.json();
 * });
 *
 * // Access reactive states
 * console.log(userResource.loading()); // true while fetching
 * console.log(userResource.error());   // Error object if fetch failed
 * console.log(userResource());         // User data when available
 *
 * // Change source triggers automatic re-fetch
 * setUserId(2); // Automatically fetches user with ID 2
 *
 * // Handle different states in effects
 * createEffect(() => {
 *   if (userResource.loading()) {
 *     showLoadingSpinner();
 *   } else if (userResource.error()) {
 *     showError(userResource.error());
 *   } else {
 *     displayUser(userResource());
 *   }
 * });
 * ```
 */
export function createResource<S, T, E = unknown>(
  source: SignalGetter<S>,
  fetcher: Fetcher<S, T>,
  options?: ReactiveOptions
): Resource<T, E> {
  const [data, setData] = createSignal<T | undefined>(undefined, options);
  const [loading, setLoading] = createSignal<boolean>(true);
  const [error, setError] = createSignal<E | undefined>(undefined);
 
  createEffect(() => {
    const sourceValue = source();
    setLoading(true);
    setError(undefined);
 
    const controller = new AbortController();
 
    // Abort the fetch if the effect re-runs or the reactive scope is disposed
    onCleanup(() => {
      controller.abort();
    });
 
    const executeFetch = async () => {
      try {
        const result = await fetcher(sourceValue, { signal: controller.signal });
        if (!controller.signal.aborted) {
          setData(result);
        }
      } catch (e) {
        if (!controller.signal.aborted) {
          setError(e as E);
        }
      } finally {
        if (!controller.signal.aborted) {
          setLoading(false);
        }
      }
    };
 
    executeFetch();
  }, options);
 
  // The main return value is a memo of the data
  const resourceMemo = createMemo(() => data(), options);
 
  // Attach the loading and error states as memos
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  (resourceMemo as any).loading = createMemo(() => loading());
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  (resourceMemo as any).error = createMemo(() => error());
 
  return resourceMemo as Resource<T, E>;
}