Guides
Query invalidation
Mark queries stale after a write, refetch the ones on screen, and select queries by factory, group or predicate, such as tags.
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. invalidateOne and invalidateMany mark queries stale and refetch the ones that are being read:
// One query: a factory and its input
await invalidateOne<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>(client: ClientFor<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>, factory: (input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, input: {
slug: string;
}): Promise<void>
Mark one query stale: a factory and its full input. If it's read, it
refetches; resolves when that settles.
invalidateOne(const qc: QueryClient<{
readonly meta: Flat<Present<RetryMeta> & Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<{
tags?: ReadonlyArray<string>;
}>>;
}>
qc, function postQuery(input: {
slug: string;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
postQuery, { slug: stringslug });
// Every query whose key starts with the `posts` group
await function invalidateMany(client: QueryClient, group: Group): Promise<void> (+3 overloads)Mark queries stale: a factory's (optionally only those whose input
matches partial), a group's (keys that start with its path, whether
the group built them or not), or those a target's where picks. Read
queries refetch now; the rest when next read. Resolves when the
refetches settle.
invalidateMany(const qc: QueryClient<{
readonly meta: Flat<Present<RetryMeta> & Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<{
tags?: ReadonlyArray<string>;
}>>;
}>
qc, const posts: Group<readonly ["posts"]>posts);
When a query is invalidated:
- It's marked stale, whatever its
staleTime. The nextloadthat starts it refetches it. - 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 or reads it.
The verbs return a promise that resolves once every refetch they 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 a group, a factory, or one query, and watch which ones refetch:
Targets
Verbs are functions that take the client first. Each …One verb takes a factory and its full input; each …Many verb takes a target:
| Call | Matches |
|---|---|
invalidateOne(qc, factory, input) | The factory's query for that input |
invalidateMany(qc, factory) | Every query of the factory |
invalidateMany(qc, factory, partialInput) | The factory's queries whose input matches partialInput |
invalidateMany(qc, group) | Every key that starts with the group, or a group in it |
invalidateMany(qc, { where }, param?) | Every query where returns true for |
invalidateMany(qc, [target, …]) | Every query any of the targets matches: several targets |
invalidateAll(qc) | Everything |
A partial input matches the input each query's factory was called with, never its key, so it works the same however the key is built: [postQuery, { slug }] for a key written as posts("bySlug", input.slug). Object fields match partially: { sort: "newest" } matches any input with that field, whatever else it has.
A query that was only hydrated has no factory until something reads it. A factory target still invalidates it, from its first read. resetMany and removeMany act at once, so a factory target skips it; a group reaches it by its key.
Targets are values, never strings: a factory matches by identity, a group by its path, and a where target, such as a tag, by what it says about each query. TypeScript checks a partial input against the factory's input, and a target's parameter against the target.
Several targets
To invalidate more than one thing, pass a list. An element is a group, a factory, a target, or [factory, partialInput] / [target, param]:
// One call: the refetches run side by side, and a query that two targets
// cover refetches once
await invalidateMany<{
readonly meta: Flat<Present<RetryMeta> & Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<{
tags?: ReadonlyArray<string>;
}>>;
}, readonly [Group<readonly ["authors"]>, (input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, readonly [(input: {
page: number;
sort: "newest" | "oldest";
}) => QueryOptions<...>, {
...;
}], readonly [...]]>(client: QueryClient<...>, targets: readonly [...]): Promise<void> (+3 overloads)
Mark queries stale: a factory's (optionally only those whose input
matches partial), a group's (keys that start with its path, whether
the group built them or not), or those a target's where picks. Read
queries refetch now; the rest when next read. Resolves when the
refetches settle.
invalidateMany(const qc: QueryClient<{
readonly meta: Flat<Present<RetryMeta> & Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<{
tags?: ReadonlyArray<string>;
}>>;
}>
qc, [
const authors: Group<readonly ["authors"]>authors, // a group
function postQuery(input: {
slug: string;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
postQuery, // a factory: all its queries
[function postsQuery(input: {
page: number;
sort: "newest" | "oldest";
}): QueryOptions<readonly ["posts", "list", {
page: number;
sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>
postsQuery, { sort?: "newest" | "oldest" | undefinedsort: "newest" }], // a factory with a partial input
[const tags: Target<Tagged, string>tags, `post:${const slug: stringslug}`], // a target with its parameter
]);
The refetches run side by side, a query that several targets cover refetches once, and the promise resolves when all of them have settled. Awaiting one verb after another serializes them instead: await invalidateMany(qc, a); await invalidateMany(qc, b); starts b's refetches only once a's are done. TypeScript checks each element on its own, the way it checks a single target.
Matching with where
A target is any object with where(query, param), typed Target<TMeta, TParam>. Its where gets each query's queryKey, input, meta and data (before select), and the parameter the verb was called with:
import type { type QueryMeta = {
readonly [x: string]: unknown;
}
A query's meta when its builder isn't bound to a root.
QueryMeta, interface Target<TMeta = Readonly<Record<string, unknown>>, TParam = never>Picks out queries by what they are: where gets each query and the
parameter the verb was called with.
Target } from "@raf-query/react";
// Every query whose key mentions a post's slug
const const mentionsPost: Target<Readonly<Record<string, unknown>>, string>mentionsPost: interface Target<TMeta = Readonly<Record<string, unknown>>, TParam = never>Picks out queries by what they are: where gets each query and the
parameter the verb was called with.
Target<type QueryMeta = {
readonly [x: string]: unknown;
}
A query's meta when its builder isn't bound to a root.
QueryMeta, string> = {
Target<Readonly<Record<string, unknown>>, string>.where: (query: QueryInfo<Readonly<Record<string, unknown>>>, param: string) => booleanwhere: (query: QueryInfo<Readonly<Record<string, unknown>>>query, postSlug: stringpostSlug) => query: QueryInfo<Readonly<Record<string, unknown>>>query.QueryInfo<Readonly<Record<string, unknown>>>.queryKey: QueryKeyqueryKey.ReadonlyArray<unknown>.includes(searchElement: unknown, fromIndex?: number): booleanDetermines whether an array includes a certain element, returning true or false as appropriate.
includes(postSlug: stringpostSlug),
};
await invalidateMany<{
readonly meta: Flat<Present<RetryMeta> & Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<{
tags?: ReadonlyArray<string>;
}>>;
}, string>(client: QueryClient<{
readonly meta: Flat<Present<RetryMeta> & Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<{
tags?: ReadonlyArray<string>;
}>>;
}>, target: Target<...>, param: string): Promise<void> (+3 overloads)
Mark queries stale: a factory's (optionally only those whose input
matches partial), a group's (keys that start with its path, whether
the group built them or not), or those a target's where picks. Read
queries refetch now; the rest when next read. Resolves when the
refetches settle.
invalidateMany(const qc: QueryClient<{
readonly meta: Flat<Present<RetryMeta> & Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<{
tags?: ReadonlyArray<string>;
}>>;
}>
qc, const mentionsPost: Target<Readonly<Record<string, unknown>>, string>mentionsPost, const slug: stringslug);
// Every query that has no data yet
await invalidateMany<{
readonly meta: Flat<Present<RetryMeta> & Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<{
tags?: ReadonlyArray<string>;
}>>;
}, never>(client: QueryClient<{
readonly meta: Flat<Present<RetryMeta> & Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<{
tags?: ReadonlyArray<string>;
}>>;
}>, target: Target<...>): Promise<void> (+3 overloads)
Mark queries stale: a factory's (optionally only those whose input
matches partial), a group's (keys that start with its path, whether
the group built them or not), or those a target's where picks. Read
queries refetch now; the rest when next read. Resolves when the
refetches settle.
invalidateMany(const qc: QueryClient<{
readonly meta: Flat<Present<RetryMeta> & Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<{
tags?: ReadonlyArray<string>;
}>>;
}>
qc, { Target<NoInfer<Flat<Present<RetryMeta> & Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<...> & Present<...>>>, never>.where: (query: QueryInfo<NoInfer<Flat<Present<RetryMeta> & Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<{
tags?: ReadonlyArray<string>;
}>>>>, param: never) => boolean
where: (query: QueryInfo<NoInfer<Flat<Present<RetryMeta> & Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<{
tags?: ReadonlyArray<string>;
}>>>>
query) => query: QueryInfo<NoInfer<Flat<Present<RetryMeta> & Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<{
tags?: ReadonlyArray<string>;
}>>>>
query.QueryInfo<TMeta = Readonly<Record<string, unknown>>>.data: unknownThe query's data, before select; undefined without data.
data === var undefinedundefined });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 and {} as its meta, so match on queryKey to include those. On a client of a typed root, query.meta has the root's fields: see Query meta.
Tags
A tag cuts across groups: every query that shows some content, wherever its key starts. raf-query has no tags of its own, because a tag is a where target over a meta field: a few lines in your app. A feature without a setup function declares meta.tags, and tag(name) matches the queries that list name:
import { function feature<TDecl extends Decl = Decl>(setup?: FeatureFunction): Feature<TDecl>A feature that declares the meta fields it reads. Without setup, it
only declares them: a root's own fields, such as an owner its targets
match on.
feature } from "@raf-query/react";
// Declares `meta.tags` on the clients that have it
const const tagged: Feature<{
meta: {
tags?: ReadonlyArray<string>;
};
}>
tagged = feature<{
meta: {
tags?: ReadonlyArray<string>;
};
}>(setup?: FeatureFunction): Feature<{
meta: {
tags?: ReadonlyArray<string>;
};
}>
A feature that declares the meta fields it reads. Without setup, it
only declares them: a root's own fields, such as an owner its targets
match on.
feature<{ meta: {
tags?: ReadonlyArray<string>;
}
meta: { tags?: readonly string[] | undefinedtags?: interface ReadonlyArray<T>ReadonlyArray<string> } }>();
type type Tagged = {
readonly tags: ReadonlyArray<string> | undefined;
}
Tagged = { readonly tags: readonly string[] | undefinedtags: interface ReadonlyArray<T>ReadonlyArray<string> | undefined };
// Every query that lists the tag
const const tag: (name: string) => Target<Tagged>tag = (name: stringname: string): interface Target<TMeta = Readonly<Record<string, unknown>>, TParam = never>Picks out queries by what they are: where gets each query and the
parameter the verb was called with.
Target<type Tagged = {
readonly tags: ReadonlyArray<string> | undefined;
}
Tagged> => ({
Target<Tagged, never>.where: (query: QueryInfo<Tagged>, param: never) => booleanwhere: (query: QueryInfo<Tagged>query) => query: QueryInfo<Tagged>query.QueryInfo<Tagged>.meta: TaggedThe meta its factory gave it. An entry only hydrated has {} until it's read.
meta.tags: readonly string[] | undefinedtags?.ReadonlyArray<string>.includes(searchElement: string, fromIndex?: number): booleanDetermines whether an array includes a certain element, returning true or false as appropriate.
includes(name: stringname) ?? false,
});
export function function makeQueryClient(): QueryClient<{
readonly meta: Flat<Present<{
tags?: ReadonlyArray<string>;
}> & Present<RetryMeta> & Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta>>;
}>
makeQueryClient() {
const { const features: readonly [Feature<{
readonly meta: FocusRefetchMeta;
}>, Feature<{
readonly meta: ReconnectRefetchMeta;
}>, Feature<{
readonly meta: PollingMeta;
}>]
features, const links: readonly [Link<{
readonly meta: RetryMeta;
}>]
links } = function recommended(): RecommendedThe behaviors most apps want: retries, refetching on focus and reconnect,
and polling. Spread it into the config:
recommended();
return createQueryClient<readonly [Link<{
readonly meta: RetryMeta;
}>], readonly [Feature<{
readonly meta: FocusRefetchMeta;
}>, Feature<{
readonly meta: ReconnectRefetchMeta;
}>, Feature<{
readonly meta: PollingMeta;
}>, Feature<{
meta: {
tags?: ReadonlyArray<string>;
};
}>]>(config?: QueryClientConfig & {
readonly features?: readonly [Feature<{
readonly meta: FocusRefetchMeta;
}>, Feature<...>, Feature<...>, Feature<...>] | undefined;
readonly links?: readonly [...] | undefined;
}): QueryClient<...>
Create a query client. Behaviors are opt-in: pass links and features,
or spread recommended(). The client's meta type is what they declare.
createQueryClient({ links?: (readonly AnyLink[] & readonly [Link<{
readonly meta: RetryMeta;
}>]) | undefined
Wrap every request, outermost first.
links, features?: (readonly AnyFeature[] & readonly [Feature<{
readonly meta: FocusRefetchMeta;
}>, Feature<{
readonly meta: ReconnectRefetchMeta;
}>, Feature<{
readonly meta: PollingMeta;
}>, Feature<{
meta: {
tags?: ReadonlyArray<string>;
};
}>]) | undefined
Behaviors started while a provider has the client mounted.
features: [...const features: readonly [Feature<{
readonly meta: FocusRefetchMeta;
}>, Feature<{
readonly meta: ReconnectRefetchMeta;
}>, Feature<{
readonly meta: PollingMeta;
}>]
features, const tagged: Feature<{
meta: {
tags?: ReadonlyArray<string>;
};
}>
tagged] });
}
export function function postBySlugQuery(input: {
slug: string;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
postBySlugQuery(input: {
slug: string;
}
input: { slug: stringslug: string }) {
return queryOptions<readonly ["posts", "bySlug", string], Post, Post>(options: BaseOptions<readonly ["posts", "bySlug", string], DefaultTypes> & {
readonly queryFn: (context: QueryFunctionContext<readonly ["posts", "bySlug", string]>) => Awaitable<Post>;
readonly select?: ((data: Post) => Post) | undefined;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes> (+1 overload)
Typed options for a query factory:
queryOptions({
BaseOptions<readonly ["posts", "bySlug", string], DefaultTypes>.queryKey: readonly ["posts", "bySlug", string]A plain array, usually built by a group(). Handles keep its literal type.
queryKey: posts<readonly ["bySlug", string]>(rest_0: "bySlug", rest_1: string): readonly ["posts", "bySlug", string]posts("bySlug", input: {
slug: string;
}
input.slug: stringslug),
queryFn: (context: QueryFunctionContext<readonly ["posts", "bySlug", string]>) => Awaitable<Post>queryFn: () => function getPost(slug: string): Promise<Post>getPost(input: {
slug: string;
}
input.slug: stringslug),
BaseOptions<readonly ["posts", "bySlug", string], DefaultTypes>.meta?: NoInfer<Partial<Readonly<Record<string, unknown>>>> | undefinedSettings for the root's links and features, and data for targets: the
fields the root's parts declare, such as retry with the retry()
link.
meta: { tags: string[]tags: ["content", `post:${input: {
slug: string;
}
input.slug: stringslug}`] },
});
}
const const qc: QueryClient<{
readonly meta: Flat<Present<{
tags?: ReadonlyArray<string>;
}> & Present<RetryMeta> & Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta>>;
}>
qc = function makeQueryClient(): QueryClient<{
readonly meta: Flat<Present<{
tags?: ReadonlyArray<string>;
}> & Present<RetryMeta> & Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta>>;
}>
makeQueryClient();
await invalidateMany<{
readonly meta: Flat<Present<{
tags?: ReadonlyArray<string>;
}> & Present<RetryMeta> & Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta>>;
}, never>(client: QueryClient<{
readonly meta: Flat<Present<{
tags?: ReadonlyArray<string>;
}> & Present<RetryMeta> & Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta>>;
}>, target: Target<...>): Promise<void> (+3 overloads)
Mark queries stale: a factory's (optionally only those whose input
matches partial), a group's (keys that start with its path, whether
the group built them or not), or those a target's where picks. Read
queries refetch now; the rest when next read. Resolves when the
refetches settle.
invalidateMany(const qc: QueryClient<{
readonly meta: Flat<Present<{
tags?: ReadonlyArray<string>;
}> & Present<RetryMeta> & Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta>>;
}>
qc, const tag: (name: string) => Target<Tagged>tag("content"));
Tagged names the field the way a client's root has it: present, and possibly undefined. Typed that way, tag() fits only clients made with tagged, and TypeScript rejects it on the others. To have one target for every tag, take the tag as the parameter:
// One target for every tag: the tag is its parameter
const const tags: Target<Tagged, string>tags: interface Target<TMeta = Readonly<Record<string, unknown>>, TParam = never>Picks out queries by what they are: where gets each query and the
parameter the verb was called with.
Target<type Tagged = {
readonly tags: ReadonlyArray<string> | undefined;
}
Tagged, string> = {
Target<Tagged, string>.where: (query: QueryInfo<Tagged>, param: string) => booleanwhere: (query: QueryInfo<Tagged>query, name: stringname) => query: QueryInfo<Tagged>query.QueryInfo<Tagged>.meta: TaggedThe meta its factory gave it. An entry only hydrated has {} until it's read.
meta.tags: readonly string[] | undefinedtags?.ReadonlyArray<string>.includes(searchElement: string, fromIndex?: number): booleanDetermines whether an array includes a certain element, returning true or false as appropriate.
includes(name: stringname) ?? false,
};
await invalidateMany<{
readonly meta: Flat<Present<RetryMeta> & Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<{
tags?: ReadonlyArray<string>;
}>>;
}, string>(client: QueryClient<{
readonly meta: Flat<Present<RetryMeta> & Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<{
tags?: ReadonlyArray<string>;
}>>;
}>, target: Target<...>, param: string): Promise<void> (+3 overloads)
Mark queries stale: a factory's (optionally only those whose input
matches partial), a group's (keys that start with its path, whether
the group built them or not), or those a target's where picks. Read
queries refetch now; the rest when next read. Resolves when the
refetches settle.
invalidateMany(const qc: QueryClient<{
readonly meta: Flat<Present<RetryMeta> & Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<{
tags?: ReadonlyArray<string>;
}>>;
}>
qc, const tags: Target<Tagged, string>tags, `post:${const slug: stringslug}`);
To pick queries by their data instead, read query.data in where. It's undefined until a query has data.
Invalidate, reset or remove
| Verbs | 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 |
Each comes as …One, …Many and …All, with the same targets; resetMany and removeMany take a list too. 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 a write, so it covers whatever the write affected. Await a verb at the end of the action:
<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 addComment(input: {
body: string;
slug: string;
}): Promise<PostComment>
addComment({
body: stringbody: var String: StringConstructor
(value?: any) => string
Allows manipulation and formatting of text strings and determination and location of substrings within strings.
String(formData: FormDataformData.FormData.get(name: string): FormDataEntryValue | nullThe get() method of the FormData interface returns the first value associated with a given key from within a FormData object. If you expect multiple values and want all of them, use the getAll() method instead.
get("body")),
slug: stringslug: props: {
slug: string;
}
props.slug: stringslug,
});
await function invalidateMany(client: QueryClient, group: Group): Promise<void> (+3 overloads)Mark queries stale: a factory's (optionally only those whose input
matches partial), a group's (keys that start with its path, whether
the group built them or not), or those a target's where picks. Read
queries refetch now; the rest when next read. Resolves when the
refetches settle.
invalidateMany(const qc: QueryClient<any>qc, const comments: Group<readonly ["posts", "comments"]>comments);
}}
>
{/* ... */}
</React.JSX.IntrinsicElements.form: React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>form>
);
}
When a write changes more than one thing, invalidate them in one call, so their refetches run side by side. Prefer invalidating a little too much over too little. Only queries being read refetch, so a broad group 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 client never revalidates cached data, because it renders once: prefetch and swr use what the cache has, even after an invalidation. A revalidate refetches an invalidated entry, so it sees the write. This matters in Next.js server actions that render again after a write.