Skip to content

raf-query is early. The API can still change between minor releases, so use it at your own risk.

raf-query

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 next prefetch or ensure refetches 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"];
FilterMatches
{}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

CallDataRead queriesUse for
invalidateKept, shown meanwhileRefetch in the backgroundAfter writes: the normal case
resetDroppedSuspend and refetchData that must not be shown again (logout)
removeDropped with the entryKeep what they rendered until they re-renderFreeing 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.