@raf-query/react 0.4.4 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +5 -1
  2. package/README.md +32 -830
  3. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -1,10 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.5](https://github.com/KATT/raf-query/compare/0.4.4...0.4.5) (2026-10-14)
4
+
5
+ * trim the READMEs, give each package its own, and warn that raf-query is early on the docs site ([#70](https://github.com/KATT/raf-query/issues/70)) ([2c06462](https://github.com/KATT/raf-query/commit/2c0646291f5fc45d2d64bc43c52f3e089c46ce05))
6
+
3
7
  ## [0.4.4](https://github.com/KATT/raf-query/compare/0.4.3...0.4.4) (2026-10-14)
4
8
 
5
9
  ### Features
6
10
 
7
- * **examples:** settings in the top bar for link prefetching and latency ([#67](https://github.com/KATT/raf-query/issues/67)) ([b8bde09](https://github.com/KATT/raf-query/commit/b8bde09f4d2525cc797bf59c35dd20b4404e8733))
11
+ - **examples:** settings in the top bar for link prefetching and latency ([#67](https://github.com/KATT/raf-query/issues/67)) ([b8bde09](https://github.com/KATT/raf-query/commit/b8bde09f4d2525cc797bf59c35dd20b4404e8733))
8
12
 
9
13
  ## [0.4.3](https://github.com/KATT/raf-query/compare/0.4.2...0.4.3) (2026-10-14)
10
14
 
package/README.md CHANGED
@@ -1,856 +1,58 @@
1
- # raf-query
1
+ # @raf-query/react
2
2
 
3
- Render-as-you-fetch data fetching for React.
3
+ Render-as-you-fetch data fetching for React: start queries in route loaders and server components, pass serializable handles to components, and read them with `use()` under Suspense.
4
4
 
5
- Queries start in route loaders and React Server Components, travel to components as small serializable **handles**, and are read with React's `use()` under Suspense. Fetch-on-render is an explicitly named escape hatch, not the default.
5
+ **[Documentation](https://raf-query.katt.dev)** · [Quick start](https://raf-query.katt.dev/docs/quick-start) · [API reference](https://raf-query.katt.dev/docs/api-reference) · [GitHub](https://github.com/KATT/raf-query)
6
6
 
7
- The cache's mechanics (dedupe, staleness, retries, infinite paging, SSR hydration) follow [TanStack Query](https://tanstack.com/query); the public API is built around loaders, RSC, and `use()` instead of `useQuery`-in-render.
7
+ > [!WARNING]
8
+ > raf-query is early. The API can still change between patch releases, and you use it at your own risk.
8
9
 
9
- The cache is written in [Effect](https://effect.website), but using raf-query doesn't take any Effect: `queryFn`s return promises, and `QueryClient`, the hooks and the integrations speak promises. Apps that do use Effect get the same cache as Effects, with typed errors and services ([Effect](#effect)).
10
+ ## Install
10
11
 
11
- ## Status
12
-
13
- Early. Both packages are on npm at `0.x`, and the API can still change between patch releases.
14
-
15
- ## Examples
16
-
17
- The same issue tracker, built on two frameworks so the integrations can be compared side by side. Each runs live on Vercel, and opens in CodeSandbox, which runs it on a cloud VM:
18
-
19
- | Example | Framework | Try it |
20
- | ----------------------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
21
- | [`examples/tanstack-start`](examples/tanstack-start#readme) | TanStack Start | [tanstack-start.raf-query.katt.dev](https://tanstack-start.raf-query.katt.dev) · [Open in CodeSandbox](https://codesandbox.io/p/devbox/github/KATT/raf-query/tree/codesandbox/examples/tanstack-start?file=/apps/tanstack-start/src/routes/issues.index.tsx) |
22
- | [`examples/nextjs`](examples/nextjs#readme) | Next.js App Router | [nextjs.raf-query.katt.dev](https://nextjs.raf-query.katt.dev) · [Open in CodeSandbox](https://codesandbox.io/p/devbox/github/KATT/raf-query/tree/codesandbox/examples/nextjs?file=/apps/nextjs/src/app/issues/page.tsx) |
23
-
24
- ## Packages
25
-
26
- | Package | Contents |
27
- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
28
- | `@raf-query/core` | Framework-agnostic and Effect-only: `QueryCache`, `queryOptions` / `infiniteQueryOptions`, handles, dehydrate / hydrate |
29
- | `@raf-query/react` | `QueryClient` (the cache with Promise methods), hooks, provider, `allKeyed`. Re-exports `@raf-query/core`, so apps import only this package |
30
-
31
- `effect` (`^4.0.0`) is a peer dependency of both, so an app that uses Effect shares one copy with raf-query. npm, pnpm and Bun install it automatically; with Yarn, add it yourself (`yarn add effect`).
32
-
33
- `@raf-query/react` subpath entries for framework integrations and tooling:
34
-
35
- | Entry | Use |
36
- | ---------------------------------- | ------------------------------------------------------------------- |
37
- | `@raf-query/react/tanstack-router` | Streams the router's query cache to the client during SSR |
38
- | `@raf-query/react/rsc` | `QueryProvider` and `HydrationBoundary` for React Server Components |
39
- | `@raf-query/react/devtools` | [Devtools](#devtools) panel for inspecting the cache in development |
40
-
41
- ## Principles
42
-
43
- - **Fetch where React recommends.** Loaders and server components start queries. Components read them.
44
- - **Promises in, `use()` out.** The read hook returns a stable promise; components unwrap it with `use()`, so reads can be conditional or happen in a child.
45
- - **The URL is the input.** Anything that changes what a query fetches (filters, sort, page) lives in path or search params, so the loader starts the right query on every navigation. Components never derive query input from local state.
46
- - **Actions for writes.** Mutations are plain async functions in form actions or transitions. The cache provides invalidation, React provides pending and optimistic state.
47
-
48
- ## Defining queries
12
+ ```bash
13
+ npm install @raf-query/react
14
+ ```
49
15
 
50
- A query is a **factory**: a function of zero or one argument that returns `queryOptions(...)` or `infiniteQueryOptions(...)`. The loader and the component call the same factory, so data types flow end to end.
16
+ `effect` is a peer dependency. npm, pnpm and Bun install it for you; with Yarn, run `yarn add @raf-query/react effect`. Apps never import it unless they write Effect code. Requires React 19.2 or newer.
51
17
 
52
- ```ts
53
- import { infiniteQueryOptions, queryOptions } from "@raf-query/react";
18
+ ## Usage
54
19
 
55
- // TanStack Start server functions (`createServerFn`); in Next.js, server actions.
56
- import { getPost, listComments } from "./posts.functions.ts";
20
+ ```tsx
21
+ import { queryOptions, useQuery } from "@raf-query/react";
57
22
 
58
23
  export function postQuery(input: { slug: string }) {
59
24
  return queryOptions({
60
25
  queryKey: ["post", input.slug],
61
- queryFn: () => getPost({ data: input }),
62
- staleTime: 30_000,
63
- });
64
- }
65
-
66
- export function commentsQuery(input: { slug: string }) {
67
- return infiniteQueryOptions({
68
- queryKey: ["comments", input.slug],
69
- queryFn: (context) =>
70
- listComments({ data: { slug: input.slug, cursor: context.pageParam } }),
71
- initialPageParam: 0,
72
- getNextPageParam: (lastPage) => lastPage.nextCursor,
26
+ queryFn: () => getPost(input.slug),
73
27
  });
74
28
  }
75
- ```
76
-
77
- Factories must take at most one argument; the type rejects two or more. The single argument is the query's **input** and must be serializable, because it is what a handle carries.
78
-
79
- A `queryFn` returns a value, a promise, or an [Effect](#effect). It receives `queryKey`, the query's [`meta`](#query-meta) and an abort `signal`; paged ones also get `pageParam` and `direction`. TypeScript infers the page type from `queryFn`, so if `getNextPageParam` comes first in the object literal (sorted keys), pass the types instead: `infiniteQueryOptions<CommentPage, number>({ ... })`.
80
-
81
- The factory is the only place a query is declared paged. Every other API — `prefetch`, `ensure`, `fetch`, `useQuery`, `useWaterfall`, `invalidate`, `setData` — takes either kind and infers the rest from the factory's type.
82
-
83
- ### Query meta
84
29
 
85
- `meta` attaches anything you want to know about a query elsewhere. It reaches the `queryFn` context, `Query.metaUnsafe(query)` (from the latest factory call) and the devtools. A common use is a per-query message for one global error handler:
86
-
87
- ```ts
88
- import { Query, QueryClient, queryOptions } from "@raf-query/react";
89
-
90
- export function issuesQuery() {
91
- return queryOptions({
92
- queryKey: ["issues"],
93
- queryFn: () => listIssues(),
94
- meta: { errorMessage: "Couldn't load issues" },
95
- });
96
- }
97
-
98
- const queryClient = new QueryClient({
99
- onError: (error, query) => {
100
- toast.error(
101
- Query.metaUnsafe(query)?.errorMessage ?? "Something went wrong",
102
- );
103
- },
104
- });
105
- ```
106
-
107
- `meta` is `Record<string, unknown>` by default. Type it for the whole app by augmenting `Register` in the package you import from (`@raf-query/react` re-exports it from `@raf-query/core`, so either works):
108
-
109
- ```ts
110
- declare module "@raf-query/react" {
111
- interface Register {
112
- queryMeta: { errorMessage?: string };
113
- }
114
- }
115
- ```
116
-
117
- `config.defaults` takes `meta` too, for queries that set none; a query's own `meta` replaces the default as a whole. `meta` is sent with dehydrated state, so keep it serializable: a hydrated entry reports the sender's `meta` until the receiving side calls its factory.
118
-
119
- ## Starting queries
120
-
121
- All three verbs live on the query client and accept a factory plus its input. They differ in what they return and what they wait for:
122
-
123
- | Call | Returns | Cache miss | Stale data | TanStack Query |
124
- | ----------------------------- | ----------------------------- | --------------------- | --------------------------------------------- | ---------------------------------------------- |
125
- | `qc.prefetch(factory, input)` | `QueryHandle` | Starts the request | Refetches in the background | `prefetchQuery`, not awaited |
126
- | `qc.ensure(factory, input)` | `Promise<EnsuredQueryHandle>` | Waits for the request | Resolves at once, refetches in the background | `ensureQueryData({ revalidateIfStale: true })` |
127
- | `qc.fetch(factory, input)` | `Promise<Data>` | Waits for the request | Waits for the refetch | `fetchQuery` |
128
-
129
- Fresh cached data is reused by all three, without a request. So `prefetch` never holds anything up, `ensure` holds up only the first visit (returning to a page shows the cached data at once, then fresh data), and `fetch` always returns fresh data, at the cost of a round trip whenever the cache is stale. On the server, data counts as stale only once `invalidate` has marked it (see below).
130
-
131
- ```tsx
132
- // TanStack Start
30
+ // The loader starts the query and passes a handle on…
133
31
  export const Route = createFileRoute("/posts/$slug")({
134
- loader: async (args) => {
135
- const qc = args.context.queryClient;
136
- const comments = qc.prefetch(commentsQuery, { slug: args.params.slug });
137
- const post = await qc.ensure(postQuery, { slug: args.params.slug });
138
- return { post, comments };
139
- },
32
+ loader: async (args) => ({
33
+ post: await args.context.queryClient.ensure(postQuery, {
34
+ slug: args.params.slug,
35
+ }),
36
+ }),
140
37
  component: PostPage,
141
38
  });
142
- ```
143
-
144
- `prefetch` and `ensure` start the request at the same moment, so neither is faster. They differ in what the user sees while it's in flight:
145
-
146
- - **`prefetch`: granular skeletons.** The navigation commits right away. Each part of the page that reads a query still loading suspends to its nearest Suspense boundary and fills in when its data streams in.
147
- - **`ensure`: a blocked navigation.** The previous page stays on screen until the data is in. In TanStack Router, a route with a `pendingComponent` shows it once `pendingMs` (1 second by default) has passed, which makes a full-page skeleton.
148
-
149
- Choose per query. `ensure` what the page can't lay out without, where a skeleton would only flash or shift (the post itself), and `prefetch` the rest (comments, sidebars, counts). Start every query before awaiting any of them, as the `prefetch` before the `await` above does, so a blocking query never delays the others.
150
-
151
- Await independent blocking queries together with `allKeyed`, which ships with raf-query. It's a ponyfill of the TC39 `Promise.allKeyed` proposal: a record of promises in, a promise of the same record out, rejecting on the first failure.
152
-
153
- ```ts
154
- import { allKeyed } from "@raf-query/react";
155
-
156
- return await allKeyed({
157
- post: qc.ensure(postQuery, { slug }),
158
- author: qc.ensure(authorQuery, { slug }),
159
- });
160
- ```
161
39
 
162
- ### Avoid dependent queries
163
-
164
- A query whose input comes from another query's result can't start until that one has finished. That's a waterfall: one extra round trip per level, on every navigation. Fix it where the data is shaped, so every query a route needs starts from what the URL already holds:
165
-
166
- - **Put the input in the route.** If the page is about an organization, its id belongs in the path (`/orgs/$orgId`), not in a lookup on the current user.
167
- - **Have the backend resolve it.** The server already knows the session and the relations, so an endpoint can return the child with its parent, or be keyed by what the client has (`GET /me/org` instead of `GET /me` then `GET /orgs/:id`).
168
-
169
- <details>
170
- <summary>Example: the current user's organization</summary>
171
-
172
- A settings page needs the user and their organization, and the organization's id is a field of the user:
173
-
174
- ```ts
175
- export const Route = createFileRoute("/settings")({
176
- loader: async (args) => {
177
- const qc = args.context.queryClient;
178
- const user = await qc.fetch(currentUserQuery);
179
- return {
180
- user: qc.prefetch(currentUserQuery),
181
- org: qc.prefetch(orgQuery, { orgId: user.orgId }),
182
- };
183
- },
184
- });
185
- ```
186
-
187
- With a 150 ms round trip, the organization can't arrive before 300 ms, and the navigation is blocked for the first 150 even though nothing on the page needed the user that early. Each query keyed by the organization (members, billing) would add another 150.
188
-
189
- Fix it in the backend: the server can find the session's organization itself, so give it an endpoint and a query with no input.
190
-
191
- ```ts
192
- export function currentOrgQuery() {
193
- return queryOptions({
194
- queryKey: ["org", "current"],
195
- queryFn: () => getCurrentOrg(),
196
- });
197
- }
198
-
199
- export const Route = createFileRoute("/settings")({
200
- loader: (args) => {
201
- const qc = args.context.queryClient;
202
- return {
203
- user: qc.prefetch(currentUserQuery),
204
- org: qc.prefetch(currentOrgQuery),
205
- };
206
- },
207
- });
208
- ```
209
-
210
- Or fix it in the routes: links into the page come from places that already know the organization (the org switcher, a list of orgs), so they can put its id in the URL.
211
-
212
- ```ts
213
- export const Route = createFileRoute("/orgs/$orgId/settings")({
214
- loader: (args) => {
215
- const qc = args.context.queryClient;
216
- return {
217
- user: qc.prefetch(currentUserQuery),
218
- org: qc.prefetch(orgQuery, { orgId: args.params.orgId }),
219
- };
220
- },
221
- });
222
- ```
223
-
224
- Either way, both requests start together, and the page is ready after one round trip instead of two. Nothing waits, so each query can be a `prefetch` or an `ensure` purely for the loading experience you want.
225
-
226
- </details>
227
-
228
- When the data really can't be reshaped (a third-party API you don't control), the loader can `await qc.fetch(...)` the parent and build the input from its data, or a component can start the child with [`useWaterfall`](#render-time-inputs-usewaterfall). Both pay the extra round trip; the loader blocks the navigation on it, the component lets the rest of the page render first.
229
-
230
- Behavior:
231
-
232
- - Every verb dedupes against the cache: a request already in flight for the entry is joined, not repeated.
233
- - A stale `ensure` doesn't hold up the navigation: the page renders the cached data and re-renders when the background refetch lands. A stale `fetch` in a loader would, so loaders use `ensure`.
234
- - Loaders run on every navigation (TanStack Router reruns them when its own `staleTime` has passed, `0` by default), so returning to a page shows what's cached at once and then fresh data. With `defaultPreload: "intent"`, also set `defaultPreloadStaleTime: 0`: otherwise the router reuses a hover's preload for 30 seconds instead of running the loader on click, and the query cache never hears about the navigation.
235
- - On the server, cached data is always reused, whatever `staleTime` says. A server client renders once, so a background refetch could only repeat a request whose result is never shown. Entries marked by `invalidate` still refetch.
236
- - A failed `prefetch` never throws out of the loader. The error surfaces where the data is read, and is reported to the client's `onError`.
237
- - A failed `ensure` or `fetch` rejects, so it reaches the route's error boundary.
238
-
239
- ## Handles
240
-
241
- A handle is the only thing that crosses the loader → component or server → client boundary:
242
-
243
- ```ts
244
- interface QueryHandle<TFactory> {
245
- readonly key: QueryKey; // ["post", "hello"]
246
- readonly input: InputOf<TFactory>; // { slug: "hello" }
247
- }
248
-
249
- // From `ensure`: the route waited for the data.
250
- interface EnsuredQueryHandle<TFactory> {
251
- readonly key: QueryKey;
252
- readonly input: InputOf<TFactory>;
253
- readonly ensured: true;
254
- }
255
- ```
256
-
257
- Handles hold no data and no functions. Data reaches the client cache through the [hydration stream](#ssr-and-hydration), so loader data stays tiny and each query is serialized once. The loader's return value lists exactly which queries a route started, and its types show which ones the route waited for: `useQuery` returns the data for an `EnsuredQueryHandle` and a promise for a `QueryHandle`. The two aren't assignable to each other, so type a prop with the one its loader returns.
258
-
259
- ## Reading queries
260
-
261
- ### `useQuery`
262
-
263
- ```ts
264
- const [promise, controls] = useQuery(factory, handle, options?);
265
- const [data, controls] = useQuery(factory, ensuredHandle, options?);
266
- ```
267
-
268
- `options` takes [`whilePending`](#when-the-url-changes) and per-read refetch settings (`refetchInterval`, `refetchOnWindowFocus`, `refetchOnReconnect`). There is no option to change the input; the handle decides what is read.
269
-
270
- `promise` is a stable `Promise<Data>` owned by the cache. Unwrap it with `use()`. When the cache already has data, the promise is pre-resolved and `use()` returns synchronously without suspending.
271
-
272
- A handle from `ensure` had its data loaded before the route rendered, so `useQuery` returns the data itself. If the entry has lost that data since (it was garbage-collected while the router kept the loader data, or `reset`), the hook suspends until it is refetched.
273
-
274
- ```tsx
40
+ // …and the component reads it.
275
41
  function PostPage() {
276
- const loaderData = Route.useLoaderData();
277
- // `post` came from `qc.ensure`, `comments` from `qc.prefetch`.
278
- const [post, postState] = useQuery(postQuery, loaderData.post);
279
-
280
- return (
281
- <article>
282
- <h1>{post.title}</h1>
283
- <button
284
- disabled={postState.isFetching}
285
- onClick={() => startTransition(() => postState.refetch())}
286
- >
287
- Refresh
288
- </button>
289
- <p>{post.body}</p>
290
- <Suspense fallback={<CommentsSkeleton />}>
291
- <Comments handle={loaderData.comments} />
292
- </Suspense>
293
- </article>
294
- );
295
- }
296
- ```
297
-
298
- `controls`:
299
-
300
- | Field | Meaning |
301
- | -------------- | ------------------------------------------------------------------------------------------------------------------------ |
302
- | `isPending` | No data yet for the handle's input; `use(promise)` will suspend |
303
- | `isPrevious` | The promise holds data for a previous input while the handle's input loads (see [`whilePending`](#when-the-url-changes)) |
304
- | `isFetching` | A request is in flight: first load, new input, or background refetch |
305
- | `isStale` | Data is older than `staleTime` |
306
- | `error` | Error from a failed background refetch. Existing data stays readable |
307
- | `updatedAt` | When the data in the promise was fetched |
308
- | `key`, `input` | Key and input of the data in the promise. They differ from the handle's while `isPrevious` is true |
309
- | `hash` | Stable string form of the handle's key, for `key` props on Suspense boundaries |
310
- | `refetch()` | Refetch now. Returns a promise, so it can be awaited inside a transition or action |
311
-
312
- When the cache updates — refetch, invalidation, `setData` — the hook returns a new, pre-resolved promise and the component re-renders without suspending. New data is structurally shared with the old: objects and arrays that are deeply equal keep their identity, and a refetch that returns equal data keeps the same promise, so nothing re-renders. Controls are tracked: reading `controls.isFetching` subscribes the component to `isFetching`, and a change to a control it never read doesn't re-render it. When the **first** load fails, the promise rejects and `use()` throws to the nearest error boundary. Resetting that boundary refetches the query.
313
-
314
- `useQuery` never refetches on mount: whatever started the query decides. Stale data refetches when a loader or server component starts the query again (`prefetch`, `ensure`, `fetch`, or hydrating the server's copy), when `useWaterfall` mounts, when invalidated, on window focus or reconnect, on `refetch()`, or on `refetchInterval`.
315
-
316
- For an app-wide indicator, `useIsFetching()` returns how many queries have a request in flight, background refetches included, and re-renders when that changes. Pass a filter to count only some: `useIsFetching({ queryKey: ["comments"] })`. It's `0` on the server and during hydration, so the indicator never appears in server HTML. Outside React, `qc.isFetching(filter?)` returns the same number.
317
-
318
- ```tsx
319
- // Rendered once, in the root layout.
320
- function GlobalProgress() {
321
- const fetching = useIsFetching();
322
- return fetching > 0 ? <ProgressBar /> : null;
42
+ const [post] = useQuery(postQuery, Route.useLoaderData().post);
43
+ return <h1>{post.title}</h1>;
323
44
  }
324
45
  ```
325
46
 
326
- ### Paged queries
327
-
328
- With an `infiniteQueryOptions` factory, `useQuery` returns the same tuple. The promise resolves to `{ pages, pageParams }`, and the controls add paging:
329
-
330
- ```tsx
331
- function Comments(props: { handle: QueryHandle<typeof commentsQuery> }) {
332
- const [pagesPromise, comments] = useQuery(commentsQuery, props.handle);
333
- const data = use(pagesPromise);
334
-
335
- return (
336
- <>
337
- {data.pages
338
- .flatMap((page) => page.items)
339
- .map((comment) => (
340
- <Comment key={comment.id} {...comment} />
341
- ))}
342
- <button
343
- disabled={!comments.hasNextPage || comments.isFetchingNextPage}
344
- onClick={() => comments.fetchNextPage()}
345
- >
346
- Load more
347
- </button>
348
- </>
349
- );
350
- }
351
- ```
352
-
353
- Paging controls: `fetchNextPage()`, `fetchPreviousPage()`, `hasNextPage`, `hasPreviousPage`, `isFetchingNextPage`, `isFetchingPreviousPage`. They exist only on the type for paged factories.
354
-
355
- On the server, `ensure` and `fetch` load the first page. Pass `{ pages: n }` as a third argument to load more up front; the option is only accepted for paged factories.
356
-
357
- ### Infinite scroll
358
-
359
- Infinite scroll is the same paged query with an automatic "Load more". A ref callback watches the button and presses it when it nears the viewport. Unmount the button while a page loads: it watches again when it remounts, so it keeps loading while it stays in reach. Its place is taken by skeletons the size of the incoming page, so the loaded rows replace them without moving anything below.
360
-
361
- ```tsx
362
- function Comments(props: { handle: QueryHandle<typeof commentsQuery> }) {
363
- const [pagesPromise, comments] = useQuery(commentsQuery, props.handle);
364
- const data = use(pagesPromise);
365
-
366
- return (
367
- <>
368
- {data.pages
369
- .flatMap((page) => page.items)
370
- .map((comment) => (
371
- <Comment key={comment.id} {...comment} />
372
- ))}
373
- {comments.isFetchingNextPage ? <CommentSkeletons /> : null}
374
- {comments.hasNextPage && !comments.isFetchingNextPage ? (
375
- <button
376
- onClick={() => comments.fetchNextPage()}
377
- ref={(button) => {
378
- if (button === null) return;
379
- const observer = new IntersectionObserver(
380
- (entries) => {
381
- if (entries.some((entry) => entry.isIntersecting)) {
382
- observer.disconnect();
383
- comments.fetchNextPage();
384
- }
385
- },
386
- { rootMargin: "0px 0px 300px 0px" },
387
- );
388
- observer.observe(button);
389
- return () => observer.disconnect();
390
- }}
391
- >
392
- Load more
393
- </button>
394
- ) : null}
395
- </>
396
- );
397
- }
398
- ```
399
-
400
- Pagination and infinite scroll differ in where the page lives:
401
-
402
- - **Pagination**: the page is a search param and part of the input, so each page is its own query and the URL links to it ([below](#when-the-url-changes)).
403
- - **Infinite scroll**: filters and sort are search params, and pages are loaded into one paged query per input. Changing a filter starts a new list from its first page.
404
-
405
- The [examples](examples/) load their issue list and comments by infinite scroll.
406
-
407
- ### When the URL changes
408
-
409
- Filters, sort order, and pagination are search params. The loader turns them into the query's input, so every navigation starts the right query and the component only ever reads the handle it was given:
410
-
411
- ```tsx
412
- import { z } from "zod";
413
-
414
- const PostsSearch = z.object({
415
- sort: z.enum(["newest", "oldest"]).default("newest"),
416
- page: z.int().positive().default(1),
417
- });
418
-
419
- export const Route = createFileRoute("/posts")({
420
- validateSearch: PostsSearch,
421
- loaderDeps: (args) => args.search,
422
- loader: (args) => ({
423
- posts: args.context.queryClient.prefetch(postsQuery, args.deps),
424
- }),
425
- component: PostsPage,
426
- });
427
-
428
- function PostsPage() {
429
- const loaderData = Route.useLoaderData();
430
- const [postsPromise, postsState] = useQuery(postsQuery, loaderData.posts, {
431
- whilePending: "previous",
432
- });
433
-
434
- return (
435
- <>
436
- <nav aria-busy={postsState.isPrevious}>
437
- <Link to="." search={(prev) => ({ ...prev, sort: "newest" })}>
438
- Newest
439
- </Link>
440
- <Link to="." search={(prev) => ({ ...prev, sort: "oldest" })}>
441
- Oldest
442
- </Link>
443
- </nav>
444
- <PostList posts={use(postsPromise)} dimmed={postsState.isPrevious} />
445
- </>
446
- );
447
- }
448
- ```
449
-
450
- Clicking a link navigates, the loader prefetches `{ sort: "oldest", page: 1 }` without blocking, and the component receives a new handle. What it renders while that input loads is set by `whilePending`:
451
-
452
- | `whilePending` | While the new input loads | Use for |
453
- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------- |
454
- | `"suspend"` (default) | `use()` suspends. TanStack Router and Next.js run navigations as React transitions, so React keeps the whole previous screen, including the old URL's active links, until the data arrives | Short loads, and pages that shouldn't show stale data |
455
- | `"previous"` | The navigation commits immediately. The promise keeps the previous input's data and `isPrevious` is true until the new data lands | Lists, filters, pagination — anything that should feel instant |
456
-
457
- Either way, an input that's already cached renders immediately. A component that mounts with a pending handle has no previous data, so `"previous"` suspends like `"suspend"` on first load.
458
-
459
- To show a skeleton instead, for instance when moving between unrelated posts, give the Suspense boundary the query's key. React treats it as new content and shows its fallback, even inside a transition:
460
-
461
- ```tsx
462
- // This loader prefetches the post instead of ensuring it.
463
- const [postPromise, postState] = useQuery(postQuery, loaderData.post);
464
-
465
- return (
466
- <Suspense key={postState.hash} fallback={<PostSkeleton />}>
467
- <PostBody post={postPromise} />
468
- </Suspense>
469
- );
470
- ```
471
-
472
- `hash` is the stable string form of the handle's key.
473
-
474
- In Next.js the same rules apply: read `searchParams` in the page, `prefetch` with them, and pass the handle to the client component.
475
-
476
- ### Passing promises down
477
-
478
- A component can call the hook with a prefetched handle and hand the promise to a child, so only the child suspends:
479
-
480
- ```tsx
481
- const [postPromise] = useQuery(postQuery, loaderData.post); // from `qc.prefetch`
482
- return (
483
- <Suspense fallback={<Spinner />}>
484
- <PostBody post={postPromise} />
485
- </Suspense>
486
- );
487
- ```
488
-
489
- A component re-renders when the promise changes, and when a control it reads changes. Controls it never reads, like `isFetching` here, don't re-render it, so a parent that only passes the promise down stays put during background refetches.
490
-
491
- ## Render-time inputs: `useWaterfall`
492
-
493
- When an input is only known at render time (an id from a parent query's result), no loader can start the query. `useWaterfall` starts it during render and returns a handle, so the consumer looks the same as one fed by a loader:
494
-
495
- ```tsx
496
- function PostAuthor(props: { post: Post }) {
497
- const author = useWaterfall(authorQuery, { id: props.post.authorId });
498
- const [authorPromise] = useQuery(authorQuery, author);
499
- return <AuthorCard author={use(authorPromise)} />;
500
- }
501
- ```
502
-
503
- Like `prefetch` in a loader, `useWaterfall` refetches stale cached data in the background, once each time the component mounts or its input changes. Re-renders don't refetch.
504
-
505
- The name is the warning: each waterfall is an extra round trip after render. First try to get the input from route params so the loader can start it, or have the backend return it with its parent.
506
-
507
- In TanStack Start the `queryFn` runs on the server during SSR, and the router streams the result. Under the `/rsc` `QueryProvider` it starts nothing on the server: the reader's Suspense boundary renders its fallback there, and the query loads in the browser. To have it in the HTML, start the dependent query in the server component instead:
508
-
509
- ```tsx
510
- const post = await qc.fetch(postQuery, { slug });
511
- const author = qc.prefetch(authorQuery, { id: post.authorId });
512
- ```
513
-
514
- For event-driven preloading, call the client directly: `onPointerEnter={() => qc.prefetch(postQuery, { slug })}`.
515
-
516
- ## Mutations
517
-
518
- There is no `useMutation`. Write data in a form action or transition, then invalidate:
519
-
520
- ```tsx
521
- function EditTitle(props: { post: Post }) {
522
- const qc = useQueryClient();
523
- const [title, setOptimisticTitle] = useOptimistic(props.post.title);
524
-
525
- return (
526
- <form
527
- action={async (formData) => {
528
- const next = String(formData.get("title"));
529
- setOptimisticTitle(next);
530
- await updatePost({ slug: props.post.slug, title: next });
531
- await qc.invalidate(postQuery, { slug: props.post.slug });
532
- }}
533
- >
534
- <input name="title" defaultValue={title} />
535
- </form>
536
- );
537
- }
538
- ```
539
-
540
- `updatePost` is a server function (or a Next.js server action). `await qc.invalidate(...)` resolves once every query currently being read has refetched, so the action's pending state lasts until the fresh data is on screen. For an error message, wrap the action in `useActionState` and return the caught error.
541
-
542
- | Call | Effect |
543
- | ------------------------------------- | ------------------------------------------------------- |
544
- | `qc.invalidate(factory, input)` | Mark one query stale and refetch it if it is being read |
545
- | `qc.invalidate(filter)` | Same, for every query the [filter](#filters) selects |
546
- | `qc.setData(factory, input, updater)` | Write data directly |
547
- | `qc.getData(factory, input)` | Read cached data, or `undefined` |
548
-
549
- ### Filters
550
-
551
- `invalidate`, `reset` and `remove` also take a filter, `{ queryKey?, exact?, predicate? }`. `queryKey` selects every query whose key starts with it, or only that key with `exact: true`; object segments match partially, so `{ queryKey: ["issues", { projectId: 3 }] }` selects every issue list in project 3. `predicate` gets each remaining `Query` and keeps the ones it returns `true` for: read its `key` and `handle.input`, or use the `Query` functions (`Query.getUnsafe(query)` for its state, `Query.observerCountUnsafe(query)`, `Query.isStaleUnsafe(query)`). Without `queryKey` every key matches, so `{}` selects the whole cache.
552
-
553
- ```ts
554
- qc.remove({ predicate: (query) => Query.observerCountUnsafe(query) === 0 }); // everything not being read
555
- ```
556
-
557
- `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.
558
-
559
- Data can be `undefined` when the `queryFn`'s type allows it (`Promise<User | undefined>`); it's cached and reused like any other value, unlike in TanStack Query, where an `undefined` result is an error. So `getData` and an updater's `previous` return `undefined` both with no data and with `undefined` data; `Query.getUnsafe(qc.getQuery(factory, input)).hasData` tells them apart.
560
-
561
- ## SSR and hydration
562
-
563
- The server's query client streams its cache to the browser's query client as entries settle. Pending queries cross as promises, so the browser resumes the server's request instead of starting a new one. Components never hydrate anything themselves; a handle only needs its key to find the entry.
564
-
565
- ### TanStack Start
566
-
567
- ```tsx
568
- // src/router.tsx
569
- import { QueryClient, QueryClientProvider } from "@raf-query/react";
570
- import { setupRouterIntegration } from "@raf-query/react/tanstack-router";
571
-
572
- export function getRouter() {
573
- const queryClient = new QueryClient();
574
- const router = createRouter({
575
- routeTree,
576
- context: { queryClient },
577
- Wrap: (props) => (
578
- <QueryClientProvider client={queryClient}>
579
- {props.children}
580
- </QueryClientProvider>
581
- ),
582
- });
583
- setupRouterIntegration({ router, queryClient });
584
- return router;
585
- }
586
- ```
587
-
588
- `setupRouterIntegration` streams queries during SSR, clears the request's cache when the response finishes, and turns `redirect()` thrown from a `queryFn` into a router navigation. `getRouter` runs per request on the server, so each request gets its own client. Options: `dehydrateOptions` (`filter`, `keepErrors`, `serializeData`), `hydrateOptions` (`deserializeData`), and `handleRedirects: false` to handle redirects yourself. By default a server error crosses to the browser as a `RedactedError` naming the query, so its message doesn't leak into the page.
589
-
590
- ### React Server Components (Next.js App Router)
591
-
592
- ```tsx
593
- // app/get-query-client.ts
594
- import { cache } from "react";
595
- import { QueryClient } from "@raf-query/react";
596
-
597
- export const getQueryClient = cache(() => new QueryClient());
598
- ```
599
-
600
- ```tsx
601
- // app/layout.tsx
602
- import { QueryProvider } from "@raf-query/react/rsc";
603
-
604
- export default function RootLayout(props: { children: React.ReactNode }) {
605
- return (
606
- <html lang="en">
607
- <body>
608
- <QueryProvider>{props.children}</QueryProvider>
609
- </body>
610
- </html>
611
- );
612
- }
613
- ```
614
-
615
- ```tsx
616
- // app/posts/[slug]/page.tsx — server component
617
- import { HydrationBoundary } from "@raf-query/react/rsc";
618
-
619
- export default async function Page(props: {
620
- params: Promise<{ slug: string }>;
621
- }) {
622
- const params = await props.params;
623
- const qc = getQueryClient();
624
-
625
- const comments = qc.prefetch(commentsQuery, { slug: params.slug });
626
- const post = await qc.fetch(postQuery, { slug: params.slug });
627
-
628
- return (
629
- <HydrationBoundary client={qc}>
630
- <article>
631
- <h1>{post.title}</h1>
632
- <Suspense fallback={<CommentsSkeleton />}>
633
- <Comments handle={comments} />
634
- </Suspense>
635
- </article>
636
- </HydrationBoundary>
637
- );
638
- }
639
- ```
640
-
641
- - `getQueryClient` is scoped to one request by React's `cache()`.
642
- - `HydrationBoundary` sends the request client's entries to the browser cache; pending ones stream as promises through the RSC payload. Render one in each server component that starts queries for client components. Entries already sent in this request are skipped.
643
- - The browser takes whichever copy is newer, by when it was fetched (`updatedAt`, so server and browser clocks should agree). When it has no data and no request of its own, it resumes the server's pending request. When it has data, or its own request in flight, it waits for the server's instead of fetching again, and takes it if newer. If it keeps its own copy (the server's is older, as in a payload the router replays from its cache on back and forward, or the server's request failed), it refetches it if stale.
644
- - `QueryProvider` creates one client per server render and one per browser tab. The server render starts no request: a client component reading a query no `HydrationBoundary` sent renders its Suspense fallback on the server, and the query loads in the browser. That boundary renders in the browser through React's error recovery, with the digest Next.js uses for `ssr: false`, so Next.js doesn't report it; other frameworks pass it to `onRecoverableError`. Pass `config` for the client's options; it must be serializable.
645
- - In a server component, `fetch` and `ensure` wait for the same thing: the per-request client only refetches what `invalidate` marked, so cached data is never stale there, and each query makes one request whatever its `staleTime`. They differ only in what they return. `await qc.fetch(...)` the data the server component renders itself, anywhere in the tree. When it also hands the query to a client component, pass `await qc.ensure(...)`: after the `fetch` it resolves from the cache, and the client component gets the data from `useQuery` without a promise.
646
- - Factories can call server actions: server components call them directly, and the browser calls them on refetch. Validate their arguments, since anyone can call them. The browser runs a page's server actions one at a time, so refetches wait for each other and for any pending mutation. For parallel requests, have the `queryFn` call a [Route Handler](https://nextjs.org/docs/app/api-reference/file-conventions/route), as [`examples/nextjs`](examples/nextjs) does.
647
-
648
- #### Server-only queries
649
-
650
- A factory that imports server-only code (a database client) can't be imported by a client component, so it can't be read with `useQuery`. Render its data in a server component, or pass a plain promise prop and `use()` it on the client. Refresh it with `router.refresh()`.
651
-
652
- ## Effect
653
-
654
- raf-query's cache is written in [Effect](https://effect.website): `QueryClient` is a Promise wrapper around a `QueryCache` service from `@raf-query/core`, available as `qc.effect`. Effect code uses the cache directly, or builds its own with `QueryCache.make(config)` (scoped: closing the scope interrupts every request and timer) or `QueryCache.layer(config)`:
655
-
656
- - A `queryFn` can return an `Effect`. Cancelling the request interrupts it.
657
- - An Effect `queryFn`'s error type is the query's: `qc.effect.fetch(issueQuery, input)` fails with `IssueNotFound | Query.CancelledError`. `ErrorOf<TFactory>` names it; it's `unknown` for promises. A `queryFn` that throws instead of returning is a defect, and isn't retried.
658
- - `retry` accepts a `Schedule` as well as a count or predicate.
659
- - `qc.effect` has every verb as an Effect: `prefetch`, `ensure`, `fetch`, `invalidate`, `reset`, `remove`, `setData`, `getData` (an `Option`), `isFetching` (a count), `dehydrate`, `hydrate`. `watch` is a `Stream` of an entry's state, and `events` a `Stream` of cache events.
660
- - SSR streaming is Effect too, and the integrations are thin wrappers over it. `dehydrateStream(options)` (scoped) returns the cache's `initial` state and a `Stream` of `updates`: entries that start or settle later, each sent once, batched per microtask. The stream ends when the scope closes. `hydrateStream(stream)` hydrates each state as it arrives. `Stream.toReadableStream` / `Stream.fromReadableStream` carry it over a web stream. The `sent` option (a `Set` of hashes) makes separate `dehydrate` calls of one request skip entries already sent.
661
- - Entries are a data type with module functions, as in Effect's own modules: `Query.fetch(query)`, `Query.refetch(query)`, `Query.invalidate(query)`, `Query.changes(query)`, and `Query.getUnsafe(query)` / `Query.subscribeUnsafe(query, listener)` for synchronous reads.
662
- - `QueryObserver` is a data type too: one reader's view of a query, as `useQuery` sees it (the controls and the promise, with `whilePending`, `refetchInterval` and the focus / reconnect options). `QueryObserver.make(query, options)` creates one, and `QueryObserver.changes(observer)` streams its results; while it runs, the query counts as read and polls on `refetchInterval`.
663
- - Time comes from the `Clock` and the page from the `Platform` service (focus, visibility, `navigator.onLine`; the default reads the globals). Provide `TestClock` or another `Platform` to test staleness, gc and offline retries without a browser.
664
-
665
- ```ts
666
- import { QueryCache, queryOptions } from "@raf-query/core";
667
- import { Effect, Schedule, Stream } from "effect";
668
-
669
- export function issueQuery(input: { id: number }) {
670
- return queryOptions({
671
- queryKey: ["issue", input.id],
672
- queryFn: () => IssueRepo.get(input.id), // Effect<Issue, IssueNotFound>
673
- retry: Schedule.exponential("200 millis").pipe(Schedule.upTo({ times: 3 })),
674
- });
675
- }
676
-
677
- const program = Effect.gen(function* () {
678
- const cache = yield* QueryCache.QueryCache;
679
- const issue = yield* cache.fetch(issueQuery, { id: 1 });
680
- yield* cache.watch(issueQuery, { id: 1 }).pipe(
681
- Stream.map((state) => state.data?.title),
682
- Stream.runForEach((title) => Effect.log(title)),
683
- );
684
- });
685
-
686
- Effect.runFork(program.pipe(Effect.provide(QueryCache.layer())));
687
- ```
688
-
689
- `watch` counts as a reader while it runs, so invalidation and window focus refetch the entry. Outside React, `cache.mount` (`Effect<void, never, Scope>`) refetches read, stale entries on window focus and reconnect while its scope is open.
690
-
691
- ### Services
692
-
693
- A `queryFn`'s Effect can require services. They're part of the factory's type, `ServicesOf<TFactory>`, and every verb that can start the query requires them:
694
-
695
- ```ts
696
- import { QueryCache, queryOptions } from "@raf-query/core";
697
- import { Context, Effect, Layer } from "effect";
698
-
699
- class IssueApi extends Context.Service<
700
- IssueApi,
701
- { readonly get: (id: number) => Effect.Effect<Issue, IssueNotFound> }
702
- >()("app/IssueApi") {}
703
-
704
- const IssueApiLive = Layer.succeed(IssueApi, { get: IssueRepo.get });
705
-
706
- export function issueQuery(input: { id: number }) {
707
- return queryOptions({
708
- queryKey: ["issue", input.id],
709
- queryFn: () =>
710
- Effect.gen(function* () {
711
- const api = yield* IssueApi;
712
- return yield* api.get(input.id);
713
- }),
714
- });
715
- }
716
-
717
- // Effect<Issue, IssueNotFound | Query.CancelledError, QueryCache | IssueApi>
718
- const issue = QueryCache.QueryCache.use((cache) =>
719
- cache.fetch(issueQuery, { id: 1 }),
720
- );
721
-
722
- Effect.runFork(
723
- issue.pipe(Effect.provide(QueryCache.layer()), Effect.provide(IssueApiLive)),
724
- );
725
- ```
726
-
727
- - On `QueryCache`, `ensure`, `fetch`, `prefetch`, `getQuery` and `watch` require the factory's services, and so does `Query.fetch`. A request runs with the services the cache was built with, under those of the latest of these calls for its entry. Requests that start without a caller (a render's read, `refetch`, polling, focus and reconnect, `invalidate`) reuse the entry's last services.
728
- - The caller's `Scope` isn't passed on: a request outlives the call that started it. Scope a `queryFn`'s resources with `Effect.scoped`.
729
- - Each request runs in a `raf-query.request` span with a `raf_query.hash` attribute, under the latest caller's span if it had one. Defects are logged with `Effect.logError`; typed failures are the query's error.
730
-
731
- `QueryClient` gets its services from a `layer`. It's built into the client's scope with the cache, so it must build synchronously. The client's type is `QueryClient<TServices>`, and its verbs accept only factories whose services the layer provides; any other factory is a type error. `dispose()` interrupts every request and runs the layer's finalizers:
732
-
733
- ```ts
734
- const queryClient = new QueryClient({ layer: IssueApiLive }); // QueryClient<IssueApi>
735
- const handle = queryClient.prefetch(issueQuery, { id: 1 });
736
-
737
- await queryClient.dispose();
738
- ```
739
-
740
- `QueryClientProvider` takes a client of any services. A hook can't know which ones, so `useQueryClient()` returns a `QueryClient<unknown>`, whose verbs accept any factory, and `useQuery` reads any factory: a request then fails with a defect if the client's layer lacks a service it needs.
741
-
742
- ## Devtools
743
-
744
- `@raf-query/react/devtools` shows the browser's query cache as it changes. Render `QueryDevtools` anywhere under the provider:
745
-
746
- ```tsx
747
- import { QueryDevtools } from "@raf-query/react/devtools";
748
-
749
- <QueryClientProvider client={queryClient}>
750
- {props.children}
751
- <QueryDevtools />
752
- </QueryClientProvider>;
753
- ```
754
-
755
- In Next.js, put it inside `QueryProvider` in the root layout. It's a client component, so a server-component layout can render it.
756
-
757
- A button in the corner opens a panel docked to the edge of the viewport. The panel lists every entry with its observer count (mounted readers) and status:
758
-
759
- | Status | Meaning |
760
- | ---------- | ------------------------------------------ |
761
- | `fetching` | A request is in flight |
762
- | `error` | The last request failed |
763
- | `stale` | Read, and older than `staleTime` |
764
- | `fresh` | Read, and within `staleTime` |
765
- | `inactive` | Nothing reads it; it's removed at `gcTime` |
766
-
767
- Select an entry to see its key, input, meta, data, error, and last update, and to **Refetch**, **Invalidate**, **Reset** (drop its data, so readers suspend and refetch) or **Remove** it. The header filters entries by key, sorts them, and has **Invalidate all** and **Clear cache**. The panel keeps its open state and size in `localStorage`.
768
-
769
- `QueryDevtools` props, all optional:
770
-
771
- | Prop | Values (default first) |
772
- | ---------------- | -------------------------------------------------------------- |
773
- | `client` | The nearest `QueryClientProvider`'s client, or another one |
774
- | `initialIsOpen` | `false`, `true`. Applies until the user opens or closes it |
775
- | `buttonPosition` | `"bottom-right"`, `"bottom-left"`, `"top-right"`, `"top-left"` |
776
- | `position` | `"bottom"`, `"top"`, `"left"`, `"right"`: the docked edge |
777
- | `theme` | `"system"`, `"dark"`, `"light"` |
778
-
779
- `QueryDevtoolsPanel` is the panel alone, to place in your own layout. It takes `client`, `theme`, `style`, and `onClose`, which adds a close button.
780
-
781
- Both render nothing on the server, since the server's cache isn't the browser's. They also render nothing when `process.env.NODE_ENV` is `"production"`, and bundlers drop the panel's code from production builds. To ship the panel in production, for example lazily behind a flag, import from `@raf-query/react/devtools/production`.
782
-
783
- ## API reference
784
-
785
- `QueryClient`:
786
-
787
- | Member | Use |
788
- | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
789
- | `new QueryClient(config?)` | `config.defaults` (query options below), `config.onError`, `config.isServer`, `config.layer` ([services](#services)) |
790
- | `prefetch`, `ensure`, `fetch` | [Start queries](#starting-queries) |
791
- | `invalidate`, `setData`, `getData` | [Write and read the cache](#mutations) |
792
- | `reset(factory, input)` / `reset(filter)` | Drop data and errors; read queries suspend and refetch. Resolves when they settle. [Filters](#filters) |
793
- | `remove(factory, input)` / `remove(filter)` | Remove entries and abort their requests. [Filters](#filters) |
794
- | `effect` | The [Effect](#effect) `QueryCache` behind the client |
795
- | `subscribe(listener)` | Cache events: `added`, `updated`, `observers` (a reader subscribed or left), `removed`, and `failed` (with the `error`) |
796
- | `isFetching(filter?)` | How many entries have a request in flight, optionally only those matching `{ queryKey, exact? }` |
797
- | `getAll()`, `getQuery(factory, input?)` | Cache entries, for devtools and integrations |
798
- | `clear()` | Drop every entry and abort in-flight requests |
799
- | `mount()` | Start window-focus and reconnect refetching. The providers call it |
800
- | `isServer` | Detected from `window`. Server clients don't retry, never gc, skip stale timers, and never revalidate cached data |
801
- | `dispose()` | Interrupt every request and release the layer's services. Resolves when its finalizers have run |
802
-
803
- Query options, accepted by `queryOptions`, `infiniteQueryOptions` and `config.defaults`:
804
-
805
- | Option | Default |
806
- | ----------------------------------------------------- | ----------------------------------------- |
807
- | `staleTime` | `0` ms |
808
- | `gcTime` | 5 minutes; `Infinity` on the server |
809
- | `retry` (count, `boolean`, predicate, `Schedule`) | `3`; `0` on the server |
810
- | `retryDelay` (ms, or a function of the failure count) | Doubling from 1 s, at most 30 s |
811
- | `refetchOnWindowFocus`, `refetchOnReconnect` | `true` (only read, stale queries refetch) |
812
- | `structuralSharing` (keep deeply equal parts of data) | `true` |
813
- | [`meta`](#query-meta) | None |
814
-
815
- `infiniteQueryOptions` also takes `getPreviousPageParam` and `maxPages`.
816
-
817
- In the browser, retries pause while `navigator.onLine` is `false`: after the retry delay, the next attempt waits for the window's `online` event. The request stays fetching meanwhile, and cancelling it (`remove`, `clear`, a restart) stops the wait. The first attempt runs regardless.
818
-
819
- When the last reader of a query unsubscribes during a background refetch, a `queryFn` that read its `signal` (or returned an Effect) is aborted: the entry keeps its data and goes back to how it was before the request, and `fetch` callers waiting on it get the cached data. Other requests, including every first load (a render may be suspended on it), finish so their result is cached, but don't retry unless a reader comes back. Requests nothing has subscribed to yet, like a loader's `prefetch`, keep running.
820
-
821
- `@raf-query/core`:
822
-
823
- | Export | Use |
824
- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
825
- | `QueryCache` | The cache service (`qc.effect`): `make`, `layer`, and the verbs as Effects |
826
- | `Query` | Cache entries: `Query.fetch`, `Query.changes`, `Query.getUnsafe`, `Query.metaUnsafe`, … |
827
- | `Platform` | The page the cache watches (focus, network); swap it in tests |
828
- | `queryOptions(options)` | Typed options for a query factory |
829
- | `infiniteQueryOptions(options)` | Typed options for a paged query factory |
830
- | `hashKey(key)`, `matchesKey(key, prefix)` | Key helpers |
831
- | `QueryObserver` | One reader's view of a query, with tracked results; for bindings |
832
- | `Query.CancelledError` | Failure of an Effect request whose entry was removed or cleared before it settled. `qc.fetch` / `qc.ensure` reject with an `AbortError` `DOMException` instead, as an aborted `fetch` does |
833
- | `RedactedError` | What a streamed pending query rejects with when its server request failed |
834
- | `QueryHandle<TFactory>` | Handle type for props |
835
- | `EnsuredQueryHandle<TFactory>` | Handle type for props fed by `ensure` |
836
- | `Thenable<T>` | Promise with `status` / `value` / `reason`, read by `use()` |
837
- | `DataOf<TFactory>`, `InputOf<TFactory>`, `ErrorOf<TFactory>` | A factory's data, input and error types |
838
- | `ServicesOf<TFactory>` | The services a factory's `queryFn` Effect needs; `never` for promises |
839
- | `Register`, `QueryMeta` | Augment `Register` with `queryMeta` to type [`meta`](#query-meta) |
840
-
841
- `@raf-query/react` (plus all of the above):
47
+ ## Entry points
842
48
 
843
- | Export | Use |
844
- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
845
- | `QueryClient` | The cache with Promise methods ([above](#api-reference)) |
846
- | `allKeyed(record)` | Await a record of promises, keeping keys |
847
- | `dehydrate(client)` / `hydrate(client, state)` | Low-level cache transfer used by the integrations |
848
- | `QueryClientProvider` | Provides a client to the tree |
849
- | `useQueryClient()` | Read the client |
850
- | `useQuery(factory, handle, options?)` | `[Thenable<Data>, controls]`; for paged factories `Data` is `{ pages, pageParams }` and controls include paging |
851
- | `useWaterfall(factory, input)` | Start a query during render and return its handle |
852
- | `useIsFetching(filter?)` | How many queries have a request in flight, re-rendering when it changes; for global loading indicators |
49
+ | Import | Use |
50
+ | ---------------------------------- | -------------------------------------------------------------------------------------------------- |
51
+ | `@raf-query/react` | `QueryClient`, `QueryClientProvider`, `useQuery`, `useWaterfall`, `useIsFetching`, and all of core |
52
+ | `@raf-query/react/tanstack-router` | [TanStack Start and Router](https://raf-query.katt.dev/docs/tanstack-start) SSR streaming |
53
+ | `@raf-query/react/rsc` | [React Server Components](https://raf-query.katt.dev/docs/nextjs), such as the Next.js App Router |
54
+ | `@raf-query/react/devtools` | The [devtools](https://raf-query.katt.dev/docs/devtools) panel, dropped from production builds |
853
55
 
854
- ## Development
56
+ ## License
855
57
 
856
- See [`.github/DEVELOPMENT.md`](.github/DEVELOPMENT.md).
58
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raf-query/react",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "description": "React bindings for raf-query, a render-as-you-fetch query library",
5
5
  "keywords": [
6
6
  "query",
@@ -19,7 +19,7 @@
19
19
  "url": "git+https://github.com/KATT/raf-query.git",
20
20
  "directory": "packages/react"
21
21
  },
22
- "homepage": "https://github.com/KATT/raf-query#readme",
22
+ "homepage": "https://raf-query.katt.dev",
23
23
  "bugs": {
24
24
  "url": "https://github.com/KATT/raf-query/issues"
25
25
  },
@@ -47,7 +47,7 @@
47
47
  }
48
48
  },
49
49
  "dependencies": {
50
- "@raf-query/core": "0.4.4"
50
+ "@raf-query/core": "0.4.5"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "@tanstack/react-router": "^1.170.0",