Skip to content

React

λ Query provides first-class React 19 bindings with components and hooks that integrate with Suspense and Transitions. All React APIs are imported from @studiolambda/query/react.

Every React application using λ Query needs a QueryProvider at the top of the tree. It provides the query instance via context and manages the broadcast channel lifecycle.

import { QueryProvider } from "@studiolambda/query/react";
function App() {
return (
<QueryProvider>
<MyApp />
</QueryProvider>
);
}

By default, QueryProvider creates a new query instance. To provide your own:

import { createQuery } from "@studiolambda/query";
import { QueryProvider } from "@studiolambda/query/react";
function App() {
const query = createQuery({
expiration: () => 10_000,
});
return (
<QueryProvider query={query}>
<MyApp />
</QueryProvider>
);
}
PropTypeDefaultDescription
queryQuerycreateQuery()The query instance to provide.
clearOnForgetbooleanfalseWhen true, forgetting a key triggers an immediate refetch. When false, stale data remains until the next manual query.
ignoreTransitionContextbooleanfalseWhen true, hooks always create their own transition instead of using a shared QueryTransition context.

The primary hook for fetching and managing data. It combines data fetching, status tracking, and mutation capabilities.

import { useQuery } from "@studiolambda/query/react";
interface Post {
id: number;
title: string;
body: string;
}
function PostCard({ id }: { id: number }) {
const { data, isPending, isExpired, refetch, mutate, forget } =
useQuery<Post>(`/api/posts/${id}`);
return (
<article style={{ opacity: isPending ? 0.7 : 1 }}>
<h2>{data.title}</h2>
<p>{data.body}</p>
<button onClick={() => refetch()}>Refresh</button>
</article>
);
}

The component suspends until data is available. After the first load, data is always the resolved value — no null checks needed.

PropertyTypeDescription
dataTThe resolved data. Always available (component suspends until ready).
isPendingbooleantrue while a React transition is in progress (refetching or mutating).
expiresAtDateWhen the cached data expires.
isExpiredbooleanWhether the data is currently expired. Auto-updates via timer.
isRefetchingbooleanWhether a background refetch is in progress.
isMutatingbooleanWhether a mutation is in progress.
refetch(options?)FunctionForce a refetch. Options override instance defaults for this call.
mutate(value, options?)FunctionMutate the cached data. Accepts a direct value or an async function.
forget()FunctionRemove this key from the cache.

useQuery accepts the same options as the core query() method, plus the QueryProvider options:

const { data } = useQuery<User>("/api/user", {
expiration: () => 30_000,
stale: false,
clearOnForget: true,
});

Use the mutate function for optimistic updates:

function UserProfile() {
const { data, mutate, isMutating } = useQuery<User>("/api/user");
async function updateName(newName: string) {
await mutate(async (previous) => {
const updated = await fetch("/api/user", {
method: "PATCH",
body: JSON.stringify({ name: newName }),
}).then((r) => r.json());
return updated;
});
}
return (
<div>
<h1>{data.name}</h1>
{isMutating && <span>Saving...</span>}
<button onClick={() => updateName("New Name")}>Rename</button>
</div>
);
}

Using an async function for mutations is preferred because it correctly triggers the isMutating state and React transitions.


Get the status of a cached key without fetching:

import { useQueryStatus } from "@studiolambda/query/react";
function CacheStatus({ cacheKey }: { cacheKey: string }) {
const { isExpired, isRefetching, isMutating, expiresAt } =
useQueryStatus(cacheKey);
return (
<div>
<p>Expired: {isExpired ? "Yes" : "No"}</p>
<p>Refetching: {isRefetching ? "Yes" : "No"}</p>
<p>Mutating: {isMutating ? "Yes" : "No"}</p>
<p>Expires at: {expiresAt?.toLocaleTimeString()}</p>
</div>
);
}

This hook subscribes to events but does not trigger any fetch. Use it for displaying cache health indicators.


Get mutation and refetch functions without subscribing to data changes:

import { useQueryActions } from "@studiolambda/query/react";
function DeleteButton({ postId }: { postId: number }) {
const { mutate, forget } = useQueryActions(`/api/posts/${postId}`);
async function handleDelete() {
await fetch(`/api/posts/${postId}`, { method: "DELETE" });
await forget();
}
return <button onClick={handleDelete}>Delete</button>;
}

This is useful when a component needs to modify cached data without displaying it — for example, a delete button in a list that doesn’t render the item’s data itself.

PropertyTypeDescription
refetch(options?)FunctionForce a refetch.
mutate(value, options?)FunctionMutate the cached data.
forget()FunctionRemove this key from the cache.

Access the raw query instance from context:

import { useQueryInstance } from "@studiolambda/query/react";
function CacheInspector() {
const query = useQueryInstance();
const keys = query.keys("items");
return (
<ul>
{keys.map((key) => (
<li key={key}>{key}</li>
))}
</ul>
);
}

Throws an error if no query instance is found in either the context or the options.


By default, each useQuery hook creates its own React transition. Use QueryTransition to share a single transition across multiple hooks, preventing visual flickering when related data updates simultaneously:

import { QueryTransition } from "@studiolambda/query/react";
import { useTransition } from "react";
function Dashboard() {
const [isPending, startTransition] = useTransition();
return (
<QueryTransition isPending={isPending} startTransition={startTransition}>
<div style={{ opacity: isPending ? 0.7 : 1 }}>
<UserPanel />
<StatsPanel />
<ActivityFeed />
</div>
</QueryTransition>
);
}

All useQuery hooks inside QueryTransition use the shared startTransition, so they all participate in the same pending state.


Prefetch cache keys when a component mounts:

import { QueryPrefetch } from "@studiolambda/query/react";
function App() {
const keys = ["/api/user", "/api/notifications"];
return (
<QueryPrefetch keys={keys}>
<Dashboard />
</QueryPrefetch>
);
}

This calls query.query(key) for each key inside a useEffect, warming the cache so child components with useQuery for these keys render instantly without suspending.


Like QueryPrefetch, but also renders <link rel="preload" as="fetch" href={key}> tags for browser-level prefetching:

import { QueryPrefetchTags } from "@studiolambda/query/react";
function App() {
const keys = ["/api/user", "/api/posts"];
return (
<QueryPrefetchTags keys={keys}>
<Content />
</QueryPrefetchTags>
);
}

The <link> tags hint to the browser to start fetching these resources early, while the useEffect warms the λ Query cache. Any additional <link> attributes can be passed as props.


The hook version of QueryPrefetch for programmatic prefetching:

import { useQueryPrefetch } from "@studiolambda/query/react";
function App() {
const keys = ["/api/dashboard/stats", "/api/dashboard/activity"];
useQueryPrefetch(keys);
return <Dashboard />;
}

Access the full context value from the nearest QueryProvider:

import { useQueryContext } from "@studiolambda/query/react";
function DebugInfo() {
const context = useQueryContext();
// context includes: query instance, clearOnForget, ignoreTransitionContext
}

Access the transition context from the nearest QueryTransition:

import { useQueryTransitionContext } from "@studiolambda/query/react";
function TransitionIndicator() {
const { isPending } = useQueryTransitionContext();
if (!isPending) return null;
return <div className="loading-bar" />;
}

Components using useQuery must be wrapped in a <Suspense> boundary (for loading states) and optionally an error boundary (for fetch failures):

import { Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary";
function App() {
return (
<QueryProvider>
<ErrorBoundary fallback={<p>Something went wrong</p>}>
<Suspense fallback={<p>Loading...</p>}>
<UserCard />
</Suspense>
</ErrorBoundary>
</QueryProvider>
);
}

After the initial suspension resolves, subsequent updates (refetching, mutations) are handled inside React transitions. The component does not re-suspend — instead, isPending becomes true while the update is in progress.