# 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:

```ts
// One query: a factory and its input
await invalidateOne(qc, postQuery, { slug });

// Every query whose key starts with the `posts` group
await invalidateMany(qc, posts);
```

When a query is invalidated:

- It's marked stale, whatever its `staleTime`. The next `load` that 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:

> [!NOTE]
> The [web version of this page](https://raf-query.katt.dev/docs/query-invalidation) has an interactive demo here. Its code: [invalidation.tsx](https://github.com/KATT/raf-query/blob/main/www/src/demos/invalidation.tsx).

## 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](https://raf-query.katt.dev/docs/query-keys.md#groups), or a group in it |
| `invalidateMany(qc, { where }, param?)`     | Every query [`where`](#matching-with-where) returns `true` for                   |
| `invalidateMany(qc, [target, …])`           | Every query any of the targets matches: [several targets](#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](https://raf-query.katt.dev/docs/ssr-and-hydration.md) 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](#tags), 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.

> [!NOTE]
> Groups and the way targets match are a design still being settled. See [Query keys](https://raf-query.katt.dev/docs/query-keys.md).

### 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]`:

```ts
// One call: the refetches run side by side, and a query that two targets
// cover refetches once
await invalidateMany(qc, [
	authors, // a group
	postQuery, // a factory: all its queries
	[postsQuery, { sort: "newest" }], // a factory with a partial input
	[tags, `post:${slug}`], // 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:

```ts
import type { QueryMeta, Target } from "@raf-query/react";

// Every query whose key mentions a post's slug
const mentionsPost: Target<QueryMeta, string> = {
	where: (query, postSlug) => query.queryKey.includes(postSlug),
};
await invalidateMany(qc, mentionsPost, slug);

// Every query that has no data yet
await invalidateMany(qc, { where: (query) => query.data === undefined });
```

`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](https://raf-query.katt.dev/docs/query-meta.md#selecting-queries-by-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](https://raf-query.katt.dev/docs/query-meta.md#links-and-features) without a setup function declares `meta.tags`, and `tag(name)` matches the queries that list `name`:

```ts
import { feature } from "@raf-query/react";

// Declares `meta.tags` on the clients that have it
const tagged = feature<{ meta: { tags?: ReadonlyArray<string> } }>();

type Tagged = { readonly tags: ReadonlyArray<string> | undefined };

// Every query that lists the tag
const tag = (name: string): Target<Tagged> => ({
	where: (query) => query.meta.tags?.includes(name) ?? false,
});

export function makeQueryClient() {
	const { features, links } = recommended();
	return createQueryClient({ links, features: [...features, tagged] });
}

export function postBySlugQuery(input: { slug: string }) {
	return queryOptions({
		queryKey: posts("bySlug", input.slug),
		queryFn: () => getPost(input.slug),
		meta: { tags: ["content", `post:${input.slug}`] },
	});
}

const qc = makeQueryClient();

await invalidateMany(qc, 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:

```ts
// One target for every tag: the tag is its parameter
const tags: Target<Tagged, string> = {
	where: (query, name) => query.meta.tags?.includes(name) ?? false,
};

await invalidateMany(qc, tags, `post:${slug}`);
```

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:

```tsx
		<form
			action={async (formData) => {
				await addComment({
					body: String(formData.get("body")),
					slug: props.slug,
				});
				await invalidateMany(qc, comments);
			}}
		>
			{/* ... */}
		</form>
	);
}
```

When a write changes more than one thing, invalidate them in [one call](#several-targets), 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](https://raf-query.katt.dev/docs/mutations.md#updates-from-mutation-responses).

## 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.
