REST APIs, React Query, data fetching
Quick Guide: React Query owns the server cache — what was fetched, how long it stays fresh, and when it is evicted. Every entry is identified by its
queryKey, andstaleTimeandgcTimeare two separate clocks rather than one. v5 removedonError/onSuccess/onSettledfromuseQuery, so error side effects belong in the component or on the sharedQueryCache.
Detailed Resources:
useQuery(generatedOptions()). Keys and types come from one source, so
a schema change surfaces as a compile error. Pattern 1.<critical_requirements>
Read the API base URL from the environment. One build then runs against every environment.
Regenerate the client when the schema changes, and commit the generated output. The regenerated types turn a breaking API change into a compile error in review rather than a runtime error in production.
</critical_requirements>
Auto-detection: useQuery, useMutation, useInfiniteQuery, useSuspenseQuery, QueryClient,
QueryClientProvider, QueryCache, MutationCache, queryKey, queryOptions, staleTime,
gcTime, invalidateQueries, placeholderData, initialPageParam, openapi-ts
Applies to:
Handled elsewhere:
Point the generator at the schema and let it emit types, service functions and query options together. The query key is generated with them, so no two call sites can disagree about it.
// openapi-ts.config.ts
export default defineConfig({
input: "./openapi.yaml",
output: "src/api-client",
plugins: ["@hey-api/typescript", "@hey-api/sdk", "@tanstack/react-query"],
});
A fetch client is bundled from @hey-api/openapi-ts v0.73 — name a client plugin explicitly only to
customise its options.
Full code: examples/core.md
Configure the base URL and the QueryClient defaults once, in a provider. Build the client inside
useState so a re-render never swaps the cache out from under the tree.
const FIVE_MINUTES_MS = 5 * 60 * 1000;
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: { staleTime: FIVE_MINUTES_MS, refetchOnWindowFocus: false },
},
}),
);
client.setConfig({ baseUrl: process.env.API_BASE_URL ?? "" });
The QueryClient half applies however the data is fetched. client is the generated client, and
its setConfig() merges into the existing config rather than replacing it, so auth and a custom
fetch can be set from separate call sites; a hand-written query function reads its base URL
wherever it already does.
Full code: examples/core.md — provider setup, static and dynamic auth, and a
fetch wrapper that aborts on a timeout
Call the generated options directly, and spread them when one call site needs a different policy.
const { data, isPending, error } = useQuery(getFeaturesOptions());
const TEN_MINUTES_MS = 10 * 60 * 1000;
const { data: slow } = useQuery({
...getFeaturesOptions(),
staleTime: TEN_MINUTES_MS,
enabled: someCondition,
});
With no generator, the queryOptions helper defines the same thing by hand, and typed:
import { queryOptions } from "@tanstack/react-query";
const featuresOptions = () =>
queryOptions({ queryKey: ["features"], queryFn: getFeatures });
Either way the key and the function are declared in one place, which is what stops two call sites
opening two cache entries for one endpoint and an invalidateQueries reaching only one of them.
Spreading the options preserves the key, so an override shares the entry rather than adding another.
Full code: examples/core.md
Component-level error covers the query a user is looking at; QueryCache and MutationCache
cover everything else from one place.
new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
// A query that already held data failed a background refetch — the component shows stale
// data and no error, so the notification is the only signal.
if (query.state.data !== undefined) notify(error.message);
},
}),
mutationCache: new MutationCache({
onError: () => notify("Operation failed."),
}),
});
Full code: examples/error-handling.md
Key the query on the debounced value, and gate it with enabled so an empty term never reaches the
network.
const DEBOUNCE_DELAY_MS = 500;
const debouncedTerm = useDebounce(searchTerm, DEBOUNCE_DELAY_MS);
const { data } = useQuery({
queryKey: ["search", debouncedTerm],
queryFn: () => searchApi(debouncedTerm),
enabled: debouncedTerm.length > 0,
});
Keying on the raw term instead caches one entry per keystroke.
</patterns><red_flags>
Breaks at runtime:
onError / onSuccess / onSettled on useQuery — removed in v5, so the side effect silently
never runs. Use the component's error or QueryCache.onError.client.setConfig() called inside a query function — it mutates config every in-flight request
shares, so concurrent queries race for the base URL.AbortController timeout with no clearTimeout on the success path — the timer keeps the
closure alive after the response has landed.error branch nor an error boundary above it — a rejected fetch
takes the subtree down.Surprising behaviour:
setConfig() merges rather than replaces, so a partial call silently keeps the previous auth.0 in v5, where v4 retried three times.fetch timeout is unrelated to staleTime and gcTime — the first bounds one request, the
other two bound the cache entry.retry left on against local mocks turns a mock that is simply missing into a slow failure.useQuery wrapper beside a generated option gives the same endpoint two query keys
and two cache entries, so invalidating one leaves the other stale.</red_flags>
npx skills add agents-inc/web-server-state-react-query下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer