Async data

Async data

createResource turns a promise into a signal. Combine it with Suspense for loading states and ErrorBoundary for failures.

createResource

tsx
import { createResource } from '@velozjs/dom';

// Without a source: fetches once.
const [user] = createResource(() => fetch('/api/me').then((r) => r.json()));

// With a source: fetches again whenever it changes.
const [post] = createResource(postId, (id) => fetch(`/api/posts/${id}`).then((r) => r.json()));

The fetcher receives the source value and { value, refetching }. It can return a promise or a plain value. While the source is false, null or undefined, nothing is fetched.

Reading a resource

tsx
post();          // the value, undefined until the first load
post.state;      // 'unresolved' | 'pending' | 'ready' | 'refreshing' | 'errored'
post.loading;    // true while a request is in flight
post.error;      // the last error
post.latest;     // the last successful value (never throws)

All of these are reactive. If the last load failed, calling post() throws, so the nearest ErrorBoundary shows its fallback.

When the source changes before a request finishes, the late response is ignored. When the owner is disposed, pending results are dropped.

Mutate and refetch

tsx
const [todos, { mutate, refetch }] = createResource(fetchTodos);

async function add(title: string) {
  mutate([...(todos() ?? []), { title, done: false }]); // optimistic
  await api.add(title);
  refetch();
}

Suspense

Suspense shows its fallback while a resource read inside it is loading for the first time.

tsx
function Profile() {
  const [user] = createResource(fetchUser);
  return <h2>{() => user()?.name}</h2>;
}

<Suspense fallback={<p>Loading…</p>}>
  {() => <Profile />}
</Suspense>
  • It waits for every resource read inside it.
  • Refetches don't bring the fallback back: the current content stays, and you can show user.loading as a small indicator.
  • The content is created once and kept alive while hidden, so its state survives.
  • Pass the children as a function so the resources are created inside the boundary.

Errors

tsx
<ErrorBoundary fallback={(error, reset) => <button onClick={reset}>{error.message}: retry</button>}>
  {() => (
    <Suspense fallback={<Spinner />}>
      {() => <Profile />}
    </Suspense>
  )}
</ErrorBoundary>

Try it

The demo on the home page uses exactly this: a resource with a source, Suspense, ErrorBoundary, refetch and a simulated failure.

On the server

renderToString doesn't wait: pending resources render their Suspense fallback. renderToStringAsync waits for the data, renders it into the HTML and returns a script that hands the values to hydrate(), so the client starts from them instead of fetching again. See Server rendering.

Resources are matched between server and client by creation order. If that order can differ, give them a stable name:

ts
createResource(fetchUser, { name: 'current-user' });