Suspense, useResource and React Query
Suspense, useResource and React Query in the React to ShadowJS guide.
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.
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.