# Links, features and meta

> Set per-query options for the client's links and features with meta, declare your own fields, and type meta by the client it belongs to.

`meta` holds a query's settings for the client's [links and features](#links-and-features), and anything else you want to know about the query elsewhere. It reaches:

- every link, as `op.meta`
- the `queryFn`, as `context.meta`
- `where` targets, as `query.meta`, to [select queries by it](#selecting-queries-by-meta)

The per-query behavior that `recommended()` turns on is set here, and each field is read by the part that does it:

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

export function postsQuery(input: { page: number; sort: "newest" | "oldest" }) {
	return queryOptions({
		queryKey: posts("list", input),
		queryFn: () => listPosts(input),
		// Read by `retry()` and `focusRefetch()`, which `recommended()` includes.
		meta: { retry: 1, refetchOnWindowFocus: false },
	});
}
```

| Field                  | Read by              | Use                                                                           |
| ---------------------- | -------------------- | ----------------------------------------------------------------------------- |
| `retry`                | `retry()`            | Attempts for this query, or `false`. See [Retries](https://raf-query.katt.dev/docs/retries.md)              |
| `refetchInterval`      | `polling()`          | Refetch every `n` ms while read. See [Polling](https://raf-query.katt.dev/docs/polling.md)                  |
| `refetchOnWindowFocus` | `focusRefetch()`     | `false` opts out. See [Window focus refetching](https://raf-query.katt.dev/docs/window-focus-refetching.md) |
| `refetchOnReconnect`   | `reconnectRefetch()` | `false` opts out                                                              |

A field does something only when the client has the part that reads it: `meta: { retry: 1 }` on a client without `retry()` is just data.

## Links and features

A client's behavior comes from two kinds of part, both passed to `createQueryClient`:

- A **link** wraps every request, like a [tRPC link](https://trpc.io/docs/client/links): it gets the request as `op` and calls `next(op)` to run the rest of the chain. The last link is the query's own `queryFn`. Links run outermost first. `retry()` and `timeout(ms)` are links.
- A **feature** is a cache-wide behavior. It starts when a provider mounts the client and stops when the last provider of that client unmounts, so it runs once however many providers share the client. `focusRefetch()`, `reconnectRefetch()` and `polling()` are features.

Each part declares the `meta` fields it reads, with `link<{ meta: … }>(fn)` or `feature<{ meta: … }>(setup)`. A feature without `setup` only declares fields: an app's own data, for targets to match on. Here a link toasts a per-query message when a request fails, and a feature declares an `owner`:

```ts
import {
	createQueryClient,
	feature,
	link,
	recommended,
} from "@raf-query/react";

// A link that reads `meta.errorMessage`: one toast per failed request.
const errorToasts = link<{ meta: { errorMessage?: string } }>(
	async (op, next) => {
		try {
			return await next(op);
		} catch (error) {
			toast.error(op.meta.errorMessage ?? "Something went wrong");
			throw error;
		}
	},
);

// A feature with no behavior: it only declares a field, for targets to match on.
const ownership = feature<{ meta: { owner?: "editorial" | "community" } }>();

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

A package that ships a link or feature needs only these exports: `link` and `feature`, the types `Link`, `Feature` and `Operation` (the request a link gets) to name them, and `Target` and `QueryInfo` for a [target](https://raf-query.katt.dev/docs/query-invalidation.md#matching-with-where) that matches on the fields it declares. [Tags](https://raf-query.katt.dev/docs/query-invalidation.md#tags) are built the same way, in a few lines of app code.

`errorToasts` comes before `retry()`, so it sees a request's error only after its retries ran out.

## Typing meta by its root

A client's **root** is the links and features it's made with, and the root's `meta` is the fields they declare. The plain `queryOptions` takes any `meta`. To check `meta` against a root, bind the builders to it once per app, in a file of their own, and import them from there instead of from `@raf-query/react`:

```tsx title="src/raf.ts"
import {
	bindInfiniteQueryOptions,
	bindQueryOptions,
	bindUseQueryClient,
} from "@raf-query/react";

// `makeQueryClient` is the function above, imported with `import type`.
type Client = typeof makeQueryClient;

export const queryOptions = bindQueryOptions<Client>();
export const useQueryClient = bindUseQueryClient<Client>();
// Only in an app that pages:
export const infiniteQueryOptions = bindInfiniteQueryOptions<Client>();
```

Each binder takes the type of the function that makes the client, or of the client. Only the type is read, so `import type` is enough and the factories don't import the client. A binder returns the plain builder, typed for the root: binding costs a few bytes, and an app that doesn't page leaves out `bindInfiniteQueryOptions` and ships none of the paging code.

Factories built with the bound `queryOptions` name no root themselves:

```tsx
export function commentsQuery(input: { slug: string }) {
	return infiniteQueryOptions({
		queryKey: comments(input.slug),
		queryFn: (context) =>
			listComments({ cursor: context.pageParam, slug: input.slug }),
		initialPageParam: 0,
		getNextPageParam: (lastPage) => lastPage.nextCursor,
		meta: {
			errorMessage: "Couldn't load the comments",
			owner: "community",
			retry: 1,
		},
	});
}
```

- `meta` takes only the fields the root's parts declare, with their types:

```tsx
queryOptions({
	queryKey: posts("bySlug", "hello-world"),
	queryFn: () => getPost("hello-world"),
	meta: { cacheFor: 60_000 },
});
```

- Verbs such as `invalidateMany` and `setData` take only that root's clients. The bound `useQueryClient` returns a client of the root, so verbs called in components check their factories too. The plain `useQueryClient()` returns a client of any root, which takes any factory.

```tsx
import { invalidateMany } from "@raf-query/react";

export function RefreshComments(props: { slug: string }) {
	const client = useQueryClient();
	return (
		<button
			onClick={() => invalidateMany(client, commentsQuery, props)}
			type="button"
		>
			Refresh
		</button>
	);
}
```

- `where` targets see the root's `meta`, as the next section shows.

The root is a function, not a client at module level, because a server makes one client per request. It's the same idea as a [tRPC v12](https://github.com/trpc/trpc/tree/v12) client typing its call options from its links.

## Selecting queries by meta

A `where` target picks queries by what their `meta` says. On a client of a typed root, `query.meta` has the root's fields:

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

const qc = makeQueryClient();

await invalidateMany(qc, {
	where: (query) => query.meta.owner === "community",
	// (property) QueryInfo<NoInfer<Flat<Present<{ errorMessage?: string; }> & Present<RetryMeta> & Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<...> & Present<...>>>>.meta: Flat<Present<{
	//     errorMessage?: string;
	// }> & Present<RetryMeta> & Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<{
	//     owner?: "editorial" | "community";
	// }>>
});
```

See [Query invalidation](https://raf-query.katt.dev/docs/query-invalidation.md#matching-with-where) for targets, and tags, which are targets built on `meta`.

## Serialization

`meta` isn't sent with dehydrated state during SSR: it comes from the factory, so it can hold anything, functions included. An entry only hydrated, which nothing has read through its factory yet, has `{}` as its `meta`. Invalidations still reach it: they're checked again when the entry is next read.
