@raf-query/react 0.8.0 → 0.9.0

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,33 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.0](https://github.com/KATT/raf-query/compare/0.8.0...0.9.0) (2026-10-15)
4
+
5
+ ### ⚠ BREAKING CHANGES
6
+
7
+ * useQuery returns the data for a handle from ensure or fetch ([#144](https://github.com/KATT/raf-query/issues/144))
8
+
9
+ ### Features
10
+
11
+ * 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)
12
+
13
+ ### Documentation
14
+
15
+ * 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))
16
+ * 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))
17
+
3
18
  ## [0.8.0](https://github.com/KATT/raf-query/compare/0.7.12...0.8.0) (2026-10-15)
4
19
 
5
20
  ### ⚠ BREAKING CHANGES
6
21
 
7
- * ensure and fetch return a QueryHandleWithData ([#142](https://github.com/KATT/raf-query/issues/142))
22
+ - ensure and fetch return a QueryHandleWithData ([#142](https://github.com/KATT/raf-query/issues/142))
8
23
 
9
24
  ### Features
10
25
 
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))
26
+ - 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
27
 
13
28
  ### Miscellaneous Chores
14
29
 
15
- * group experiments under `.experimental/<topic>/` ([#137](https://github.com/KATT/raf-query/issues/137)) ([52ce752](https://github.com/KATT/raf-query/commit/52ce7522223346e82687338d6ac6d34f793b6fcf))
30
+ - group experiments under `.experimental/<topic>/` ([#137](https://github.com/KATT/raf-query/issues/137)) ([52ce752](https://github.com/KATT/raf-query/commit/52ce7522223346e82687338d6ac6d34f793b6fcf))
16
31
 
17
32
  ## [0.7.12](https://github.com/KATT/raf-query/compare/0.7.11...0.7.12) (2026-10-15)
18
33
 
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
 
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 };
@@ -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.0",
4
4
  "description": "React bindings for raf-query, a render-as-you-fetch query library",
5
5
  "keywords": [
6
6
  "query",
@@ -46,7 +46,7 @@
46
46
  }
47
47
  },
48
48
  "dependencies": {
49
- "@raf-query/core": "0.8.0"
49
+ "@raf-query/core": "0.9.0"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "@tanstack/react-router": "^1.170.0",