[]previous and next page

Suspense, useResource and React Query

Suspense, useResource and React Query in the React to ShadowJS guide.

For agentsUpdated

useResource is the data primitive. Suspense shows a fallback while a resource read inside it loads for the first time. This replaces use(promise), Suspense with a cache, and the basic half of React Query.

src/user.tsxtsx
import { useResource, useStore } from "@shadow-js/core";
import { Show, Suspense } from "@shadow-js/web";

export interface User {
  id: number;
  name: string;
}

export function UserCard(props: { load: (id: number, signal: AbortSignal) => Promise<User> }) {
  const [id, setId] = useStore<number | null>(1);
  const user = useResource(id, (value, { signal }) => props.load(value, signal));
  return (
    <section>
      <Suspense fallback={<p>loading</p>}>
        <h2>{user()?.name}</h2>
      </Suspense>
      <Show when={user.state === "refreshing"}>
        <small>updating</small>
      </Show>
      <button type="button" onClick={() => setId((current) => (current === null ? 1 : current + 1))}>
        next
      </button>
      <button type="button" onClick={() => user.refetch()}>
        reload
      </button>
      <button type="button" onClick={() => setId(null)}>
        pause
      </button>
    </section>
  );
}

useResource(source, fetcher) fetches whenever source() changes. While the source is false, null or undefined the resource stays unresolved and does not fetch, which replaces React Query's enabled: false. The fetcher receives (source, { signal, refetching, value }); signal is aborted when a newer fetch starts or the owner is disposed, and value is the last loaded value. Without a source, useResource(fetcher) fetches once.

Resource also has loading, error, latest (the last good value; reading it never throws or suspends), refetch() and mutate(value). refetch() returns a promise of the new value; it resolves with undefined when the fetch failed or a newer one replaced it, and it never rejects, so a call you do not await is safe. mutate(value) sets the value locally, cancels a fetch in flight and returns the new value. The source of useResource(source, fetcher) may also be a plain value, which fetches once for that value.

Choose a resource or a shared query cache#

useResource owns one request and its loading state. For React Query-style server state shared across components, use @shadow-js/query. Keep route-owned data in router loaders.

React Query feature ShadowJS 1.0.0
Shared query keys and request deduplication useQuery(key, fetcher) on one query client shares the cache and in-flight request.
staleTime, gcTime Query options control freshness and unused-cache retention.
Retries and backoff retry and retryDelay; optional jitter with exponentialBackoff.
Focus and reconnect refresh refetchOnWindowFocus, refetchOnReconnect and refetchOnMount.
invalidateQueries client.invalidate(filter) marks matching queries stale and refreshes mounted readers.
Mutations and optimistic updates useMutation, with onMutate, onError and onSettled for update, rollback and refresh.
Infinite queries useInfiniteQuery with page parameters.
initialData, placeholderData, keepPreviousData, select Query options provide initial values, placeholders, retained results and memoized selection.
Offline writes and persistence Add the queue and persist features only when the application needs them.
A one-off async value Core useResource stays smaller when a shared cache is unnecessary.

Transitions#

useTransition, useDeferredValue and startTransition have the same names and meaning in ShadowJS. Start with Transitions.

Optimistic updates#

useOptimistic(source, reducer) takes the authoritative accessor and a reducer, and returns [state, apply]. Call apply(action, promise) before you start the work: state shows the reducer applied over source() for every action whose promise is still pending, then falls back to source() when it settles, whichever way it settles.