CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-solid-js

A declarative JavaScript library for building user interfaces with fine-grained reactivity.

Pending
Overview
Eval results
Files

resources-async.mddocs/

Resources and Async

Resource system for handling asynchronous data loading with built-in loading states, error handling, and automatic refetching capabilities.

Capabilities

Creating Resources

Create reactive resources that wrap promises and provide loading states.

/**
 * Creates a resource without a source signal
 * @param fetcher - Function that returns data or a promise
 * @param options - Configuration options for the resource
 * @returns Resource tuple with data accessor and actions
 */
function createResource<T, R = unknown>(
  fetcher: ResourceFetcher<true, T, R>,
  options?: ResourceOptions<T, true>
): ResourceReturn<T, R>;

/**
 * Creates a resource with a source signal that triggers refetching
 * @param source - Source signal that triggers refetching when it changes
 * @param fetcher - Function that fetches data based on source value
 * @param options - Configuration options for the resource
 * @returns Resource tuple with data accessor and actions
 */
function createResource<T, S, R = unknown>(
  source: ResourceSource<S>,
  fetcher: ResourceFetcher<S, T, R>,
  options?: ResourceOptions<T, S>
): ResourceReturn<T, R>;

type ResourceSource<S> = S | false | null | undefined | (() => S | false | null | undefined);
type ResourceFetcher<S, T, R = unknown> = (
  source: S,
  info: ResourceFetcherInfo<T, R>
) => T | Promise<T>;

interface ResourceFetcherInfo<T, R = unknown> {
  value: T | undefined;
  refetching: R | boolean;
}

interface ResourceOptions<T, S = unknown> {
  initialValue?: T;
  name?: string;
  deferStream?: boolean;
  ssrLoadFrom?: "initial" | "server";
  storage?: (init: T | undefined) => [Accessor<T | undefined>, Setter<T | undefined>];
  onHydrated?: (k: S | undefined, info: { value: T | undefined }) => void;
}

Usage Examples:

import { createResource, createSignal, Show, Suspense } from "solid-js";

// Simple resource without source
const [data] = createResource(async () => {
  const response = await fetch("/api/users");
  return response.json();
});

// Resource with source signal
function UserProfile() {
  const [userId, setUserId] = createSignal<number | null>(null);
  
  const [user] = createResource(userId, async (id) => {
    if (!id) return null;
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) throw new Error("User not found");
    return response.json();
  });

  return (
    <div>
      <input 
        type="number" 
        placeholder="Enter user ID"
        onInput={(e) => setUserId(parseInt(e.target.value) || null)}
      />
      
      <Show when={userId()}>
        <Suspense fallback={<div>Loading user...</div>}>
          <Show when={user()} fallback={<div>User not found</div>}>
            {(userData) => (
              <div>
                <h2>{userData.name}</h2>
                <p>Email: {userData.email}</p>
              </div>
            )}
          </Show>
        </Suspense>
      </Show>
    </div>
  );
}

Resource States and Actions

Resources provide various states and actions for managing async data.

type ResourceReturn<T, R = unknown> = [
  resource: Resource<T>,
  actions: {
    mutate: Setter<T | undefined>;
    refetch: (info?: R) => T | Promise<T> | undefined | null;
  }
];

type Resource<T> = Accessor<T | undefined> & {
  state: "unresolved" | "pending" | "ready" | "refreshing" | "errored";
  loading: boolean;
  error: any;
  latest: T | undefined;
};

Usage Examples:

import { createResource, createSignal, Show, ErrorBoundary } from "solid-js";

function DataManager() {
  const [refreshTrigger, setRefreshTrigger] = createSignal(0);
  
  const [data, { mutate, refetch }] = createResource(
    refreshTrigger,
    async () => {
      console.log("Fetching data...");
      const response = await fetch("/api/data");
      if (!response.ok) throw new Error("Failed to fetch");
      return response.json();
    },
    { initialValue: [] }
  );

  return (
    <div>
      <div class="toolbar">
        <button 
          onClick={() => refetch()}
          disabled={data.loading}
        >
          {data.loading ? "Refetching..." : "Refetch"}
        </button>
        
        <button 
          onClick={() => setRefreshTrigger(prev => prev + 1)}
        >
          Trigger Refresh
        </button>
        
        <button 
          onClick={() => mutate(prev => [...(prev || []), { id: Date.now(), name: "New Item" }])}
        >
          Add Item (Optimistic)
        </button>
      </div>

      <div class="status">
        <p>State: {data.state}</p>
        <p>Loading: {data.loading ? "Yes" : "No"}</p>
        <Show when={data.error}>
          <p class="error">Error: {data.error.message}</p>
        </Show>
      </div>

      <ErrorBoundary fallback={(err) => <div>Error: {err.message}</div>}>
        <Show when={data()} fallback={<div>No data</div>}>
          {(items) => (
            <ul>
              <For each={items}>
                {(item) => <li>{item.name}</li>}
              </For>
            </ul>
          )}
        </Show>
      </ErrorBoundary>
    </div>
  );
}

Suspense Integration

Resources integrate seamlessly with Suspense for declarative loading states.

/**
 * Tracks all resources inside a component and renders a fallback until they are all resolved
 * @param props - Suspense component props
 * @returns JSX element with loading state management
 */
function Suspense(props: {
  fallback?: JSX.Element;
  children: JSX.Element;
}): JSX.Element;

Usage Examples:

import { createResource, Suspense, ErrorBoundary, For } from "solid-js";

function App() {
  const [users] = createResource(() => fetchUsers());
  const [posts] = createResource(() => fetchPosts());

  return (
    <div>
      <h1>My App</h1>
      
      <ErrorBoundary fallback={(err) => <div>Error: {err.message}</div>}>
        <Suspense fallback={<div>Loading application data...</div>}>
          <div class="content">
            <section>
              <h2>Users</h2>
              <For each={users()}>
                {(user) => <div class="user">{user.name}</div>}
              </For>
            </section>
            
            <section>
              <h2>Posts</h2>
              <For each={posts()}>
                {(post) => <div class="post">{post.title}</div>}
              </For>
            </section>
          </div>
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

async function fetchUsers() {
  await new Promise(resolve => setTimeout(resolve, 1000));
  return [{ id: 1, name: "John" }, { id: 2, name: "Jane" }];
}

async function fetchPosts() {
  await new Promise(resolve => setTimeout(resolve, 1500));
  return [{ id: 1, title: "Post 1" }, { id: 2, title: "Post 2" }];
}

Advanced Resource Patterns

Handle complex async scenarios with advanced resource patterns.

Usage Examples:

import { createResource, createSignal, createMemo, batch } from "solid-js";

// Dependent resources
function UserDashboard() {
  const [userId, setUserId] = createSignal(1);
  
  const [user] = createResource(userId, async (id) => {
    const response = await fetch(`/api/users/${id}`);
    return response.json();
  });
  
  // Posts depend on user data
  const [posts] = createResource(
    () => user()?.id,
    async (userId) => {
      if (!userId) return [];
      const response = await fetch(`/api/users/${userId}/posts`);
      return response.json();
    }
  );

  return (
    <Suspense fallback={<div>Loading dashboard...</div>}>
      <div>
        <Show when={user()}>
          {(userData) => (
            <div>
              <h1>{userData.name}'s Dashboard</h1>
              <Suspense fallback={<div>Loading posts...</div>}>
                <For each={posts()}>
                  {(post) => <div class="post">{post.title}</div>}
                </For>
              </Suspense>
            </div>
          )}
        </Show>
      </div>
    </Suspense>
  );
}

// Resource with caching
function CachedDataExample() {
  const cache = new Map();
  
  const [cacheKey, setCacheKey] = createSignal("default");
  
  const [data] = createResource(
    cacheKey,
    async (key) => {
      if (cache.has(key)) {
        console.log("Cache hit for", key);
        return cache.get(key);
      }
      
      console.log("Fetching", key);
      const response = await fetch(`/api/data/${key}`);
      const result = await response.json();
      cache.set(key, result);
      return result;
    }
  );

  return (
    <div>
      <select onChange={(e) => setCacheKey(e.target.value)}>
        <option value="default">Default</option>
        <option value="users">Users</option>
        <option value="posts">Posts</option>
      </select>
      
      <Suspense fallback={<div>Loading {cacheKey()}...</div>}>
        <pre>{JSON.stringify(data(), null, 2)}</pre>
      </Suspense>
    </div>
  );
}

// Resource with retry logic
function RetryableResource() {
  const [retryCount, setRetryCount] = createSignal(0);
  
  const [data, { refetch }] = createResource(
    retryCount,
    async (count) => {
      console.log(`Attempt ${count + 1}`);
      
      // Simulate random failures
      if (Math.random() < 0.7) {
        throw new Error(`Failed on attempt ${count + 1}`);
      }
      
      return { message: `Success on attempt ${count + 1}!` };
    }
  );

  const retry = () => {
    setRetryCount(prev => prev + 1);
  };

  return (
    <div>
      <ErrorBoundary 
        fallback={(err, reset) => (
          <div class="error">
            <p>Error: {err.message}</p>
            <button onClick={() => { reset(); retry(); }}>
              Retry (Attempt {retryCount() + 2})
            </button>
          </div>
        )}
      >
        <Suspense fallback={<div>Loading (Attempt {retryCount() + 1})...</div>}>
          <Show when={data()}>
            {(result) => (
              <div class="success">
                <p>{result.message}</p>
                <button onClick={retry}>Try Again</button>
              </div>
            )}
          </Show>
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

Server-Side Rendering

Resources support server-side rendering with hydration capabilities.

// Resource with SSR support
const [data] = createResource(
  () => fetchData(),
  {
    ssrLoadFrom: "server", // Load from server on SSR
    deferStream: true,     // Defer streaming until resolved
    onHydrated: (key, info) => {
      console.log("Resource hydrated", key, info);
    }
  }
);

// Custom storage for persistence
const [persistedData] = createResource(
  () => fetchData(),
  {
    storage: (init) => {
      // Custom storage implementation
      const [value, setValue] = createSignal(init);
      
      // Save to localStorage when value changes
      createEffect(() => {
        const current = value();
        if (current !== undefined) {
          localStorage.setItem("cached-data", JSON.stringify(current));
        }
      });
      
      // Load from localStorage on mount
      onMount(() => {
        const stored = localStorage.getItem("cached-data");
        if (stored) {
          setValue(JSON.parse(stored));
        }
      });
      
      return [value, setValue];
    }
  }
);

Types

Resource Types

type Resource<T> = Accessor<T | undefined> & {
  state: "unresolved" | "pending" | "ready" | "refreshing" | "errored";
  loading: boolean;
  error: any;
  latest: T | undefined;
};

type ResourceActions<T, R = unknown> = {
  mutate: Setter<T>;
  refetch: (info?: R) => T | Promise<T> | undefined | null;
};

type ResourceReturn<T, R = unknown> = [Resource<T>, ResourceActions<T | undefined, R>];

type InitializedResource<T> = Accessor<T> & {
  state: "ready" | "refreshing" | "errored";
  loading: boolean;
  error: any;
  latest: T;
};

type InitializedResourceReturn<T, R = unknown> = [
  resource: InitializedResource<T>,
  actions: {
    mutate: Setter<T>;
    refetch: (info?: R) => T | Promise<T> | undefined | null;
  }
];

interface InitializedResourceOptions<T, S = unknown> extends ResourceOptions<T, S> {
  initialValue: T;
}

Fetcher Types

type ResourceFetcher<S, T> = (
  source: S,
  info: ResourceFetcherInfo<T>
) => T | Promise<T>;

interface ResourceFetcherInfo<T> {
  value: T | undefined;
  refetching: boolean | unknown;
}

type ResourceSource<S> = S | false | null | (() => S | false | null);

Install with Tessl CLI

npx tessl i tessl/npm-solid-js

docs

component-system.md

context-scoping.md

control-flow.md

index.md

reactive-primitives.md

resources-async.md

store-management.md

web-rendering.md

tile.json