Guides
Query invalidation
Mark queries stale after a write, refetch the ones on screen, and select queries with key prefixes and predicates.
Waiting for data to go stale on its own doesn't work after a write: the user just changed something and expects to see it. invalidate marks queries stale and refetches the ones that are being read:
// One query
await const qc: QueryClient<never>qc.QueryClient<never>.invalidate<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>(...target: Target<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>): Promise<void>
invalidate(function postQuery(input: {
slug: string;
}): QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>
postQuery, { slug: stringslug });
// Every query whose key starts with ["posts"]
await const qc: QueryClient<never>qc.QueryClient<never>.invalidate<QueryFactory<never>>(...target: Target<QueryFactory<never>>): Promise<void>invalidate({ Filter.queryKey?: QueryKey | undefinedqueryKey: ["posts"] });When a query is invalidated:
- It's marked stale, whatever its
staleTime. The nextprefetchorensurerefetches it, even on the server. - If something is reading it, it refetches now, in the background. Readers keep showing the current data and re-render when the new data lands.
- If nothing is reading it, nothing happens until the next time something starts it.
invalidate returns a promise that resolves once every refetch it started has settled. Await it at the end of an action, and the action stays pending until the fresh data is on screen.
Try it. Each list below reads its own query. Invalidate by prefix, or exactly, and watch which ones refetch:
Query filters
invalidate, reset and remove take either a factory and its input, or a filter:
interface Filter {
Filter.queryKey?: QueryKey | undefinedqueryKey?: type QueryKey = readonly unknown[]QueryKey; // key prefix; every key matches without it
Filter.exact?: boolean | undefinedexact?: boolean; // match `queryKey` exactly instead of as a prefix
Filter.predicate?: ((query: Query) => boolean) | undefinedpredicate?: (query: Queryquery: type Query = QueryModule.Query<QueryFactory>Query) => boolean; // runs after key matching
}Given these keys:
["todos"];
["todos", { status: stringstatus: "done" }];
["todos", { status: stringstatus: "done", page: numberpage: 2 }];
["todos", 5];
["users"];| Filter | Matches |
|---|---|
{} | Everything |
{ queryKey: ["todos"] } | All four todos keys |
{ queryKey: ["todos"], exact: true } | Only ["todos"] |
{ queryKey: ["todos", { status: "done" }] } | Both done lists |
{ queryKey: ["todos", 5] } | Only ["todos", 5] |
Object segments match partially: { status: "done" } matches any object segment with that field, whatever else it has. This is why query keys go from general to specific, with an object of filters last: one prefix then reaches every variant.
Matching with predicate
predicate receives each Query that passed the key match. Read its key and handle.input, or its state with the Query functions:
// Every todo list on page 2 or later
await const qc: QueryClient<never>qc.QueryClient<never>.invalidate<QueryFactory<never>>(...target: Target<QueryFactory<never>>): Promise<void>invalidate({
Filter.predicate?: ((query: Query.Query) => boolean) | undefinedpredicate: (query: Query.Query<QueryFactory>query) =>
query: Query.Query<QueryFactory>query.Query<QueryFactory>.key: QueryKeykey[0] === "todos" &&
typeof (query: Query.Query<QueryFactory>query.Query<QueryFactory>.handle: QueryHandle<QueryFactory>handle.QueryHandle<QueryFactory>.input: unknowninput as { page?: unknownpage?: unknown } | undefined)?.page?: unknownpage ===
"number" &&
(query: Query.Query<QueryFactory>query.Query<QueryFactory>.handle: QueryHandle<QueryFactory>handle.QueryHandle<QueryFactory>.input: unknowninput as { page: numberpage: number }).page: numberpage >= 2,
});
// Everything not being read
const qc: QueryClient<never>qc.QueryClient<never>.remove<QueryFactory<never>>(...target: Target<QueryFactory<never>>): voidremove({ Filter.predicate?: ((query: Query.Query) => boolean) | undefinedpredicate: (query: Query.Query<QueryFactory>query) => import QueryQuery.function observerCountUnsafe<QueryFactory>(self: Query.Query<QueryFactory>): numberobserverCountUnsafe(query: Query.Query<QueryFactory>query) === 0 });handle.input is the input of the latest factory call for that entry. An entry that was only hydrated, and not yet read or started on this client, has undefined input, so match on key to include those.
Invalidate, reset or remove
| Call | Data | Read queries | Use for |
|---|---|---|---|
invalidate | Kept, shown meanwhile | Refetch in the background | After writes: the normal case |
reset | Dropped | Suspend and refetch | Data that must not be shown again (logout) |
remove | Dropped with the entry | Keep what they rendered until they re-render | Freeing memory, dropping an abandoned key |
reset resolves when the refetches it caused settle, like invalidate. remove is synchronous, and aborts in-flight requests.
Invalidating from mutations
The common pattern is to invalidate at the end of an action, so it covers whatever the write affected:
<React.JSX.IntrinsicElements.form: React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>form
React.FormHTMLAttributes<HTMLFormElement>.action?: string | ((formData: FormData) => void | Promise<void>) | undefinedaction={async (formData: FormDataformData) => {
await function addTodo(text: string): Promise<void>addTodo(var String: StringConstructor
(value?: any) => string
String(formData: FormDataformData.FormData.get(name: string): FormDataEntryValue | nullget("text")));
await const qc: QueryClient<unknown>qc.QueryClient<unknown>.invalidate<QueryFactory<unknown>>(...target: Target<QueryFactory<unknown>>): Promise<void>invalidate({ Filter.queryKey?: QueryKey | undefinedqueryKey: ["todos"] });
}}
>
{/* ... */}
</React.JSX.IntrinsicElements.form: React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>form>
);
}Prefer invalidating a little too much over too little. Only queries being read refetch, so a broad prefix costs a request per list on screen, not per list ever loaded.
When the server's response has everything the cache needs, write it with setData instead and skip the refetch. See Mutations.
Invalidation on the server
A server render's cache never considers data stale, because it renders once. invalidate is the exception: an entry marked by invalidate refetches on the server, too. This matters in Next.js server actions that render again after a write.