Data loading and the query cache
The @netscript/fresh/query subpath gives islands a package-owned data layer
built on TanStack Query, while the package root exposes a small set of
cache-entry helpers that page loaders and islands share. Reach for this surface
when an island needs cached, refetchable, or live data, and when a server loader
wants to prime that cache before the island hydrates.
The two surfaces
Data loading in NetScript Fresh spans two import points:
@netscript/fresh/query— TanStack Query hooks for islands. Island code imports from this subpath rather than from@tanstack/preact-querydirectly, which centralizes the dependency and enables framework-level enhancements. It provides the island provider (QueryIsland), the sharedQueryClientsingleton (getIslandQueryClient), the query and mutation hooks (useIslandQuery,useIslandMutation, and friends), live-query hooks (useLiveQuery,useLiveSuspenseQuery), and hydration utilities for streaming SSR.@netscript/fresh(root) — the cross-cutting page-loader cache helpers. These operate on plainCacheEntryLikevalues: adatapayload plus acachedAtUnix-epoch timestamp in milliseconds. They let a loader reason about whether its cache is complete and how fresh it is before deciding to serve cached data or refetch.
Every other capability lives on its own explicit subpath (./builders,
./route, ./form, ./defer, ./server, ./streams, ./interactive,
./vite, ./error, and ./testing).
Cache entries at the loader
A CacheEntryLike<T> is the shared cache shape across page loaders and partial
orchestration:
interface CacheEntryLike<T> {
readonly data: T;
readonly cachedAt: number; // Unix epoch ms
}
The root helpers operate over arrays of these entries:
hasAllCacheEntries(entries)returnstruewhen every supplied entry is present.nullandundefinedentries count as missing, so a loader can pass the raw results of several cache reads and learn in one call whether the page can be served entirely from cache.minCachedAt(entries)returns the oldestcachedAttimestamp across the supplied entries, orundefinedwhen there are none. Use it to compute the effective freshness of a composed page — the page is only as fresh as its stalest entry.projectCachedItemFromList(listEntry, predicate)projects a single cached item out of aCachedListEntryLike<TItem>(an entry whosedataholds anitemsarray) while preserving the list'scachedAttimestamp. This lets a detail view reuse a list response already in cache instead of issuing a new fetch.
A cache-first load pattern
A loader can read its cache entries, check completeness, and derive freshness before deciding whether to refetch:
import {
hasAllCacheEntries,
minCachedAt,
projectCachedItemFromList,
type CacheEntryLike,
type CachedListEntryLike,
} from "@netscript/fresh";
interface Widget {
id: string;
name: string;
}
// Cache entries gathered earlier in the request.
const widgetList: CachedListEntryLike<Widget> | undefined = readWidgetList();
const summary: CacheEntryLike<number> | undefined = readSummary();
const entries = [widgetList, summary];
if (hasAllCacheEntries(entries)) {
const oldest = minCachedAt(entries); // freshness floor, in epoch ms
const selected = projectCachedItemFromList(
widgetList,
(item) => item.id === "w-1",
);
// Serve cached data along with its freshness floor.
return { widget: selected?.data, cachedAt: oldest };
}
// Otherwise, fall through to a fresh load.
Querying inside an island
Islands wrap their content in QueryIsland, the island-level TanStack Query
provider, then call the package-owned hooks. useIslandQuery runs a query
through the shared client; its options come from IslandQueryOptions:
| Option | Meaning |
|---|---|
queryKey |
Stable cache key (QueryKey, a readonly unknown[]). |
queryFn |
Function that loads query data. |
initialData |
Initial data supplied from a Fresh server loader. |
enabled |
Whether the query should run automatically. |
staleTime |
Cache freshness duration in milliseconds. |
gcTime |
Cache garbage-collection duration in milliseconds. |
select |
Optional projection applied by the query adapter. |
onError |
Optional error callback. |
The hook returns an IslandQueryResult with data, error, status,
isLoading, isSuccess, isError, and a refetch() method.
import { QueryIsland, useIslandQuery } from "@netscript/fresh/query";
interface Widget {
id: string;
name: string;
}
function WidgetView() {
const query = useIslandQuery<Widget[]>({
queryKey: ["widgets"],
queryFn: () => fetch("/api/widgets").then((res) => res.json()),
staleTime: 30_000,
});
if (query.isLoading) return <p>Loading…</p>;
if (query.isError) return <p>Could not load widgets.</p>;
return (
<ul>
{query.data?.map((widget) => <li key={widget.id}>{widget.name}</li>)}
</ul>
);
}
export default function WidgetIsland() {
return (
<QueryIsland>
<WidgetView />
</QueryIsland>
);
}
initialData is the bridge between a server loader and the island: pass the
loader's cached payload as initialData so the island renders with data
immediately and only refetches once staleTime elapses.
Mutations, infinite queries, and live data
useIslandMutationruns a mutation through the shared client. Its options (IslandMutationOptions) take amutationFnplus optionalonSuccessandonErrorcallbacks; the result exposesmutate,mutateAsync,status,isPending,data, anderror.useIslandInfiniteQueryloads paged data. Its options extend the query options withinitialPageParamandgetNextPageParam; the result addshasNextPage,isFetchingNextPage, andfetchNextPage().useIslandSuspenseQueryanduseIslandSuspenseInfiniteQueryare the Suspense variants; the suspense query result guaranteesdatais present for rendered consumers.useLiveQueryanduseLiveSuspenseQueryaccept anIslandLiveQueryFactoryand an optional dependency array, returning anIslandLiveQueryResultwithdata,status,error, and adetailsrecord of preserved upstream fields.
Shorter aliases — useQuery, useMutation, useInfiniteQuery,
useSuspenseQuery, and useSuspenseInfiniteQuery — map to their useIsland*
counterparts for backward compatibility.
Server prefetch and hydration
For streaming SSR, prefetch on the server and hand the cache to the island:
- Prefetch into a per-request
QueryClient, then calldehydrateQueryClient(queryClient)to produce a serializableDehydratedState(itsqueriesandmutationsarrays). - Serialize that state into the document with
QueryHydrationScript, or pass it directly to an island. - On the client, restore it with
HydrationBoundary(which reads either astateprop or a script tag byid) or callhydrateFromDehydratedagainst the island client.
getIslandQueryClient() returns the shared singleton (it throws if called
during SSR outside island hydration — use a per-request client for prefetch).
useQueryClient returns the active handle inside an island, and
resetIslandQueryClient() clears the singleton, primarily for tests.
API summary
Root cache helpers (@netscript/fresh):
| Symbol | Description |
|---|---|
hasAllCacheEntries |
Return true when every supplied entry is present. |
minCachedAt |
Return the oldest cachedAt timestamp across the supplied entries. |
projectCachedItemFromList |
Project a single cached item from a cached list while preserving the timestamp. |
CacheEntryLike<T> |
Cached-entry shape: data payload plus cachedAt epoch-ms timestamp. |
CachedListEntryLike<TItem> |
Cached list-entry shape with a data.items array, used for projection. |
Query subpath (@netscript/fresh/query):
| Symbol | Description |
|---|---|
QueryIsland |
Island-level TanStack Query provider. |
useIslandQuery |
Run an island query through the shared QueryClient. |
useIslandMutation |
Run an island mutation through the shared QueryClient. |
useIslandInfiniteQuery |
Run an island infinite (paged) query. |
useIslandSuspenseQuery |
Suspense variant of the island query. |
useLiveQuery |
Run an island live query through the query surface. |
useLiveSuspenseQuery |
Suspense variant of the live query. |
getIslandQueryClient |
Get (or create) the shared island QueryClient. |
useQueryClient |
Return the active island QueryClient handle. |
resetIslandQueryClient |
Reset the island QueryClient singleton (testing). |
useIsFetching / useIsMutating |
Count active island queries / mutations. |
dehydrateQueryClient |
Dehydrate a QueryClient into serializable state. |
hydrateFromDehydrated |
Hydrate a client from server-dehydrated state. |
QueryHydrationScript |
Render a JSON script tag of dehydrated state. |
HydrationBoundary |
Hydrate the island client from state or a script tag. |
IslandQueryOptions |
Options for useIslandQuery. |
IslandQueryResult |
Result of an island query hook call. |
DehydratedState |
State produced by server-side query dehydration. |
QueryKey |
Stable query-key shape (readonly unknown[]). |
LoaderData |
Resolve the awaited return value of a Fresh route loader. |
Related
How server pages compose and load.
Routing and route contractsRoute definitions and loaders.
Interactive islandsWhere query hooks run on the client.
Deferred and streaming UIStream data into hydrating islands.
Server-validated formsPair mutations with form handling.
Live dashboard tutorialBuild a cache-first, live-updating page.
See also the Web Layer hub.