@raf-query/react 0.8.0 → 0.9.1

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.
package/CHANGELOG.md CHANGED
@@ -1,18 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.1](https://github.com/KATT/raf-query/compare/0.9.0...0.9.1) (2026-10-15)
4
+
5
+ ### Features
6
+
7
+ * **react:** add @raf-query/react/react-router for React Router Framework Mode ([#146](https://github.com/KATT/raf-query/issues/146)) ([4e02c80](https://github.com/KATT/raf-query/commit/4e02c809d8be1eea33935c9f0b6abc38cbdce25c)), references [#130](https://github.com/KATT/raf-query/issues/130) [#148](https://github.com/KATT/raf-query/issues/148) [#148](https://github.com/KATT/raf-query/issues/148)
8
+
9
+ ### Miscellaneous Chores
10
+
11
+ * **lint:** ban member access on use(), in code and Markdown fences ([#155](https://github.com/KATT/raf-query/issues/155)) ([b7b37b4](https://github.com/KATT/raf-query/commit/b7b37b4f6d83c0a232697d05b177c33bdea11df1)), references [#140](https://github.com/KATT/raf-query/issues/140)
12
+
13
+ ## [0.9.0](https://github.com/KATT/raf-query/compare/0.8.0...0.9.0) (2026-10-15)
14
+
15
+ ### ⚠ BREAKING CHANGES
16
+
17
+ - useQuery returns the data for a handle from ensure or fetch ([#144](https://github.com/KATT/raf-query/issues/144))
18
+
19
+ ### Features
20
+
21
+ - useQuery returns the data for a handle from ensure or fetch ([#144](https://github.com/KATT/raf-query/issues/144)) ([1845244](https://github.com/KATT/raf-query/commit/18452443ba61c205a53c4f71d60ec99a9b8620a7)), references [#142](https://github.com/KATT/raf-query/issues/142)
22
+
23
+ ### Documentation
24
+
25
+ - call the early API a note about minor releases ([#143](https://github.com/KATT/raf-query/issues/143)) ([2d79eed](https://github.com/KATT/raf-query/commit/2d79eed7b107b6d16898ebb203a9bdfcfb3856c4))
26
+ - show prefetch and ensure, and make list queries infinite ([#140](https://github.com/KATT/raf-query/issues/140)) ([25cfaa0](https://github.com/KATT/raf-query/commit/25cfaa08f9f192af7efded1269f4ae8a9e5f1026))
27
+
3
28
  ## [0.8.0](https://github.com/KATT/raf-query/compare/0.7.12...0.8.0) (2026-10-15)
4
29
 
5
30
  ### ⚠ BREAKING CHANGES
6
31
 
7
- * ensure and fetch return a QueryHandleWithData ([#142](https://github.com/KATT/raf-query/issues/142))
32
+ - ensure and fetch return a QueryHandleWithData ([#142](https://github.com/KATT/raf-query/issues/142))
8
33
 
9
34
  ### Features
10
35
 
11
- * ensure and fetch return a QueryHandleWithData ([#142](https://github.com/KATT/raf-query/issues/142)) ([504cd2a](https://github.com/KATT/raf-query/commit/504cd2a19e04b614eb648f02b256128c4eb31d38))
36
+ - ensure and fetch return a QueryHandleWithData ([#142](https://github.com/KATT/raf-query/issues/142)) ([504cd2a](https://github.com/KATT/raf-query/commit/504cd2a19e04b614eb648f02b256128c4eb31d38))
12
37
 
13
38
  ### Miscellaneous Chores
14
39
 
15
- * group experiments under `.experimental/<topic>/` ([#137](https://github.com/KATT/raf-query/issues/137)) ([52ce752](https://github.com/KATT/raf-query/commit/52ce7522223346e82687338d6ac6d34f793b6fcf))
40
+ - group experiments under `.experimental/<topic>/` ([#137](https://github.com/KATT/raf-query/issues/137)) ([52ce752](https://github.com/KATT/raf-query/commit/52ce7522223346e82687338d6ac6d34f793b6fcf))
16
41
 
17
42
  ## [0.7.12](https://github.com/KATT/raf-query/compare/0.7.11...0.7.12) (2026-10-15)
18
43
 
package/README.md CHANGED
@@ -4,22 +4,23 @@ Render-as-you-fetch data fetching for React: start queries in route loaders and
4
4
 
5
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
- > [!WARNING]
8
- > raf-query is early. The API can still change between patch releases, and you use it at your own risk.
7
+ > [!NOTE]
8
+ > raf-query is early. The API can still change between minor releases, and you use it at your own risk.
9
9
 
10
10
  ## Install
11
11
 
12
12
  ```bash
13
- npm install @raf-query/react
13
+ npm install @raf-query/react effect
14
14
  ```
15
15
 
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.
16
+ Apps never import `effect` unless they write Effect code. Requires React 19.2 or newer.
17
17
 
18
18
  ## Usage
19
19
 
20
+ A query is a factory. The loader and the component call the same one. A paged list, such as comments, uses `infiniteQueryOptions`.
21
+
20
22
  ```tsx
21
- import { queryOptions, useQuery } from "@raf-query/react";
22
- import { use } from "react";
23
+ import { infiniteQueryOptions, queryOptions } from "@raf-query/react";
23
24
 
24
25
  export function postQuery(input: { slug: string }) {
25
26
  return queryOptions({
@@ -28,20 +29,96 @@ export function postQuery(input: { slug: string }) {
28
29
  });
29
30
  }
30
31
 
31
- // The loader starts the query and passes a handle on…
32
+ export function commentsQuery(input: { slug: string }) {
33
+ return infiniteQueryOptions({
34
+ queryKey: ["comments", input.slug],
35
+ queryFn: (context) =>
36
+ listComments({ cursor: context.pageParam, slug: input.slug }),
37
+ initialPageParam: 0,
38
+ getNextPageParam: (lastPage) => lastPage.nextCursor,
39
+ });
40
+ }
41
+ ```
42
+
43
+ **`prefetch`** starts the request and returns a handle at once. The page renders now; `use()` suspends whatever is still loading, under its Suspense fallback.
44
+
45
+ ```tsx
46
+ import { type QueryHandle, useQuery } from "@raf-query/react";
47
+ import { Suspense, use } from "react";
48
+
49
+ export const Route = createFileRoute("/posts/$slug")({
50
+ loader: (args) => {
51
+ const qc = args.context.queryClient;
52
+ const { slug } = args.params;
53
+ return {
54
+ comments: qc.prefetch(commentsQuery, { slug }),
55
+ post: qc.prefetch(postQuery, { slug }),
56
+ };
57
+ },
58
+ component: PostPage,
59
+ });
60
+
61
+ function PostPage() {
62
+ const data = Route.useLoaderData();
63
+ const [postPromise] = useQuery(postQuery, data.post);
64
+ const post = use(postPromise);
65
+ return (
66
+ <article>
67
+ <h1>{post.title}</h1>
68
+ <Suspense fallback={<CommentsSkeleton />}>
69
+ <Comments handle={data.comments} />
70
+ </Suspense>
71
+ </article>
72
+ );
73
+ }
74
+
75
+ function Comments(props: { handle: QueryHandle<typeof commentsQuery> }) {
76
+ const [pagesPromise] = useQuery(commentsQuery, props.handle);
77
+ const pages = use(pagesPromise);
78
+ return pages.pages
79
+ .flatMap((page) => page.items)
80
+ .map((comment) => (
81
+ <p key={comment.id}>
82
+ <strong>{comment.author}</strong> {comment.body}
83
+ </p>
84
+ ));
85
+ }
86
+ ```
87
+
88
+ **`ensure`** waits until the data is cached, so the navigation holds. `useQuery` returns the data itself for that handle, not a promise. Await the post and its comments together with [`allKeyed`](https://raf-query.katt.dev/docs/starting-queries#awaiting-several-queries) — a record of promises in, the same record out — so one wait never delays the other.
89
+
90
+ ```tsx
91
+ import { allKeyed, useQuery } from "@raf-query/react";
92
+
32
93
  export const Route = createFileRoute("/posts/$slug")({
33
- loader: async (args) => ({
34
- post: await args.context.queryClient.ensure(postQuery, {
35
- slug: args.params.slug,
36
- }),
37
- }),
94
+ loader: (args) => {
95
+ const qc = args.context.queryClient;
96
+ const { slug } = args.params;
97
+ return allKeyed({
98
+ comments: qc.ensure(commentsQuery, { slug }),
99
+ post: qc.ensure(postQuery, { slug }),
100
+ });
101
+ },
38
102
  component: PostPage,
39
103
  });
40
104
 
41
- // …and the component reads it.
42
105
  function PostPage() {
43
- const [post] = useQuery(postQuery, Route.useLoaderData().post);
44
- return <h1>{use(post).title}</h1>;
106
+ const data = Route.useLoaderData();
107
+ // Both already waited, so these are the data itself.
108
+ const [post] = useQuery(postQuery, data.post);
109
+ const [pages] = useQuery(commentsQuery, data.comments);
110
+ return (
111
+ <article>
112
+ <h1>{post.title}</h1>
113
+ {pages.pages
114
+ .flatMap((page) => page.items)
115
+ .map((comment) => (
116
+ <p key={comment.id}>
117
+ <strong>{comment.author}</strong> {comment.body}
118
+ </p>
119
+ ))}
120
+ </article>
121
+ );
45
122
  }
46
123
  ```
47
124
 
@@ -52,6 +129,7 @@ function PostPage() {
52
129
  | `@raf-query/react` | `QueryClient`, `QueryClientProvider`, `useQuery`, `useWaterfall`, `useIsFetching`, and all of core |
53
130
  | `@raf-query/react/tanstack-router` | [TanStack Start and Router](https://raf-query.katt.dev/docs/tanstack-start) SSR streaming |
54
131
  | `@raf-query/react/rsc` | [React Server Components](https://raf-query.katt.dev/docs/nextjs), such as the Next.js App Router |
132
+ | `@raf-query/react/react-router` | [React Router](https://raf-query.katt.dev/docs/react-router) Framework Mode loaders |
55
133
  | `@raf-query/react/devtools` | The [devtools](https://raf-query.katt.dev/docs/devtools) panel; omit the element to tree-shake it |
56
134
 
57
135
  ## License
package/dist/index.d.mts CHANGED
@@ -2,7 +2,7 @@ import { QueryClient, QueryClientConfig, dehydrate, hydrate } from "./client.mjs
2
2
  import { Settled, allKeyed } from "./all-keyed.mjs";
3
3
  import { QueryClientProvider, useQueryClient } from "./provider.mjs";
4
4
  import { useIsFetching } from "./use-is-fetching.mjs";
5
- import { HasTypedError, PagingControls, QueryControls, ThrowsOnError, UseQueryControls, UseQueryOptions, UseQueryResult, UseQueryThrowOptions, useQuery } from "./use-query.mjs";
5
+ import { HasTypedError, PagingControls, QueryControls, ThrowsOnError, UseQueryControls, UseQueryOptions, UseQueryResult, UseQueryResultWithData, UseQueryThrowOptions, useQuery } from "./use-query.mjs";
6
6
  import { useWaterfall } from "./use-waterfall.mjs";
7
7
  export * from "@raf-query/core";
8
- export { type HasTypedError, type PagingControls, QueryClient, type QueryClientConfig, QueryClientProvider, type QueryControls, type Settled, type ThrowsOnError, type UseQueryControls, type UseQueryOptions, type UseQueryResult, type UseQueryThrowOptions, allKeyed, dehydrate, hydrate, useIsFetching, useQuery, useQueryClient, useWaterfall };
8
+ export { type HasTypedError, type PagingControls, QueryClient, type QueryClientConfig, QueryClientProvider, type QueryControls, type Settled, type ThrowsOnError, type UseQueryControls, type UseQueryOptions, type UseQueryResult, type UseQueryResultWithData, type UseQueryThrowOptions, allKeyed, dehydrate, hydrate, useIsFetching, useQuery, useQueryClient, useWaterfall };
@@ -0,0 +1,35 @@
1
+ import { DehydratedState, HydrateOptions } from "@raf-query/core";
2
+ import { ReactNode } from "react";
3
+ //#region src/react-router.d.ts
4
+ /**
5
+ * Writes a loader's dehydrated cache into the browser (or SSR) client.
6
+ * Use it with React Router Framework Mode: start queries in the loader,
7
+ * `dehydrate` the request's client, and pass that state here.
8
+ *
9
+ * ```tsx
10
+ * export async function loader(args: Route.LoaderArgs) {
11
+ * const qc = new QueryClient();
12
+ * const post = await qc.ensure(postQuery, { slug: args.params.slug });
13
+ * return { dehydratedState: dehydrate(qc), post };
14
+ * }
15
+ *
16
+ * export default function Page(props: Route.ComponentProps) {
17
+ * const { dehydratedState, post } = props.loaderData;
18
+ * return (
19
+ * <HydrationBoundary state={dehydratedState}>
20
+ * <Post handle={post} />
21
+ * </HydrationBoundary>
22
+ * );
23
+ * }
24
+ * ```
25
+ *
26
+ * Pending entries cross as promises when the framework's serializer
27
+ * supports them (React Router's Single Fetch does). Nested boundaries
28
+ * are fine: each only writes what its `state` holds.
29
+ */
30
+ export declare function HydrationBoundary(props: {
31
+ readonly children?: ReactNode;
32
+ readonly options?: HydrateOptions;
33
+ readonly state: DehydratedState;
34
+ }): import("react").JSX.Element;
35
+ //#endregion
@@ -0,0 +1,39 @@
1
+ "use client";
2
+ import { Hydrate } from "./hydrate.mjs";
3
+ import { jsx } from "react/jsx-runtime";
4
+ //#region src/react-router.tsx
5
+ /**
6
+ * Writes a loader's dehydrated cache into the browser (or SSR) client.
7
+ * Use it with React Router Framework Mode: start queries in the loader,
8
+ * `dehydrate` the request's client, and pass that state here.
9
+ *
10
+ * ```tsx
11
+ * export async function loader(args: Route.LoaderArgs) {
12
+ * const qc = new QueryClient();
13
+ * const post = await qc.ensure(postQuery, { slug: args.params.slug });
14
+ * return { dehydratedState: dehydrate(qc), post };
15
+ * }
16
+ *
17
+ * export default function Page(props: Route.ComponentProps) {
18
+ * const { dehydratedState, post } = props.loaderData;
19
+ * return (
20
+ * <HydrationBoundary state={dehydratedState}>
21
+ * <Post handle={post} />
22
+ * </HydrationBoundary>
23
+ * );
24
+ * }
25
+ * ```
26
+ *
27
+ * Pending entries cross as promises when the framework's serializer
28
+ * supports them (React Router's Single Fetch does). Nested boundaries
29
+ * are fine: each only writes what its `state` holds.
30
+ */
31
+ function HydrationBoundary(props) {
32
+ return /* @__PURE__ */ jsx(Hydrate, {
33
+ options: props.options,
34
+ state: props.state,
35
+ children: props.children
36
+ });
37
+ }
38
+ //#endregion
39
+ export { HydrationBoundary };
@@ -1,4 +1,4 @@
1
- import { DataOf, ErrorOf, InputOf, IsPaged, QueryFactory, QueryHandle, QueryKey, QueryObserver } from "@raf-query/core";
1
+ import { DataOf, ErrorOf, InputOf, IsPaged, QueryFactory, QueryHandle, QueryHandleWithData, QueryKey, QueryObserver } from "@raf-query/core";
2
2
  import { Result } from "effect";
3
3
  //#region src/use-query.d.ts
4
4
  /** `refetchInterval`, `refetchOnWindowFocus`, `refetchOnReconnect`, `whilePending`. */
@@ -63,6 +63,8 @@ export type HasTypedError<TFactory extends QueryFactory> = unknown extends Error
63
63
  */
64
64
  export type ThrowsOnError<TFactory extends QueryFactory, TThrow extends boolean | undefined> = HasTypedError<TFactory> extends false ? true : TThrow extends boolean ? TThrow : false;
65
65
  export type UseQueryResult<TFactory extends QueryFactory, TThrow extends boolean | undefined = undefined> = ThrowsOnError<TFactory, TThrow> extends true ? readonly [promise: Promise<DataOf<TFactory>>, controls: UseQueryControls<TFactory>] : readonly [promise: Promise<Result.Result<DataOf<TFactory>, ErrorOf<TFactory>>>, controls: UseQueryControls<TFactory>];
66
+ /** {@link useQuery}'s result for a handle from `ensure` or `fetch`. */
67
+ export type UseQueryResultWithData<TFactory extends QueryFactory, TThrow extends boolean | undefined = undefined> = ThrowsOnError<TFactory, TThrow> extends true ? readonly [data: DataOf<TFactory>, controls: UseQueryControls<TFactory>] : readonly [result: Result.Result<DataOf<TFactory>, ErrorOf<TFactory>>, controls: UseQueryControls<TFactory>];
66
68
  /**
67
69
  * Read the query a handle points at. Returns a promise to unwrap with
68
70
  * `use()` and the query's controls:
@@ -87,11 +89,25 @@ export type UseQueryResult<TFactory extends QueryFactory, TThrow extends boolean
87
89
  * The component re-renders when the promise changes, and when a control it
88
90
  * has read changes; changes to controls it never reads are skipped.
89
91
  *
90
- * A handle from `ensure` or `fetch` reads the same way: the hook reads the
91
- * live entry, not the handle's `data` snapshot, and its promise is already
92
- * fulfilled unless the entry has lost its data since (garbage-collected,
93
- * `reset`).
92
+ * A handle from `ensure` or `fetch` already has its data, so the hook
93
+ * unwraps the promise itself and returns the data (or `Result`):
94
+ *
95
+ * ```tsx
96
+ * const [post, postState] = useQuery(postQuery, loaderData.post);
97
+ * ```
98
+ *
99
+ * It reads the live entry, not the handle's `data` snapshot. If that entry
100
+ * has lost its data since (garbage-collected, `reset`), the hook suspends
101
+ * until it is refetched.
102
+ *
103
+ * The hook picks by the handle at runtime (`data` in it), so a prop that
104
+ * receives a handle from `ensure` / `fetch` must be typed
105
+ * {@link QueryHandleWithData}: typed as `QueryHandle`, it reads as a promise
106
+ * while the hook returns the data.
94
107
  */
108
+ export declare function useQuery<TFactory extends QueryFactory, const TThrow extends boolean | undefined = undefined>(factory: TFactory, handle: QueryHandleWithData<TFactory>, options?: UseQueryOptions & UseQueryThrowOptions<TFactory> & {
109
+ readonly throwOnError?: TThrow;
110
+ }): UseQueryResultWithData<TFactory, TThrow>;
95
111
  export declare function useQuery<TFactory extends QueryFactory, const TThrow extends boolean | undefined = undefined>(factory: TFactory, handle: QueryHandle<TFactory>, options?: UseQueryOptions & UseQueryThrowOptions<TFactory> & {
96
112
  readonly throwOnError?: TThrow;
97
113
  }): UseQueryResult<TFactory, TThrow>;
@@ -3,7 +3,7 @@ import { useQueryClient } from "./provider.mjs";
3
3
  import { assertSent } from "./browser-requests.mjs";
4
4
  import { Query, QueryObserver, fulfilled, pending } from "@raf-query/core";
5
5
  import { Effect, Result } from "effect";
6
- import { useEffect, useState, useSyncExternalStore } from "react";
6
+ import { use, useEffect, useState, useSyncExternalStore } from "react";
7
7
  //#region src/use-query.ts
8
8
  function queryFor(client, factory, input) {
9
9
  const args = [input];
@@ -87,7 +87,14 @@ function useQuery(factory, handle, options = {}) {
87
87
  ]);
88
88
  QueryObserver.trackProp(observer, "promise");
89
89
  const controls = withPromiseCommands(QueryObserver.trackResult(observer, result));
90
- if (!(options.throwOnError ?? client.throwOnError)) return [asResultThenable(result.promise), controls];
90
+ const throwOnError = options.throwOnError ?? client.throwOnError;
91
+ const withData = "data" in handle;
92
+ if (!throwOnError) {
93
+ const resultPromise = asResultThenable(result.promise);
94
+ if (withData) return [use(resultPromise), controls];
95
+ return [resultPromise, controls];
96
+ }
97
+ if (withData) return [use(result.promise), controls];
91
98
  return [result.promise, controls];
92
99
  }
93
100
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raf-query/react",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "React bindings for raf-query, a render-as-you-fetch query library",
5
5
  "keywords": [
6
6
  "query",
@@ -35,6 +35,7 @@
35
35
  ".": "./dist/index.mjs",
36
36
  "./devtools": "./dist/devtools.mjs",
37
37
  "./rsc": "./dist/rsc.mjs",
38
+ "./react-router": "./dist/react-router.mjs",
38
39
  "./tanstack-router": {
39
40
  "workerd": "./dist/tanstack-router.mjs",
40
41
  "worker": "./dist/tanstack-router.mjs",
@@ -46,7 +47,7 @@
46
47
  }
47
48
  },
48
49
  "dependencies": {
49
- "@raf-query/core": "0.8.0"
50
+ "@raf-query/core": "0.9.1"
50
51
  },
51
52
  "peerDependencies": {
52
53
  "@tanstack/react-router": "^1.170.0",