@raf-query/react 0.7.12 → 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,15 +1,44 @@
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
+
18
+ ## [0.8.0](https://github.com/KATT/raf-query/compare/0.7.12...0.8.0) (2026-10-15)
19
+
20
+ ### ⚠ BREAKING CHANGES
21
+
22
+ - ensure and fetch return a QueryHandleWithData ([#142](https://github.com/KATT/raf-query/issues/142))
23
+
24
+ ### Features
25
+
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))
27
+
28
+ ### Miscellaneous Chores
29
+
30
+ - group experiments under `.experimental/<topic>/` ([#137](https://github.com/KATT/raf-query/issues/137)) ([52ce752](https://github.com/KATT/raf-query/commit/52ce7522223346e82687338d6ac6d34f793b6fcf))
31
+
3
32
  ## [0.7.12](https://github.com/KATT/raf-query/compare/0.7.11...0.7.12) (2026-10-15)
4
33
 
5
- * add knip with every issue type as an error ([#129](https://github.com/KATT/raf-query/issues/129)) ([603719d](https://github.com/KATT/raf-query/commit/603719dcb834312d4a813ecae865df7d56faf8c5))
6
- * cache Next.js Turbopack builds ([#128](https://github.com/KATT/raf-query/issues/128)) ([667b38f](https://github.com/KATT/raf-query/commit/667b38ff5728dad3fdad6d90445aea480cfbd063))
7
- * explain how a route loads its data on the homepage ([#121](https://github.com/KATT/raf-query/issues/121)) ([ec1d6ef](https://github.com/KATT/raf-query/commit/ec1d6efdad75ea2d05dd56790e5e980756afea13))
8
- * frame the cache as async data, not server-state-only ([#126](https://github.com/KATT/raf-query/issues/126)) ([db9ba02](https://github.com/KATT/raf-query/commit/db9ba02995aa01cadacb2739fd258de3c920d440))
34
+ - add knip with every issue type as an error ([#129](https://github.com/KATT/raf-query/issues/129)) ([603719d](https://github.com/KATT/raf-query/commit/603719dcb834312d4a813ecae865df7d56faf8c5))
35
+ - cache Next.js Turbopack builds ([#128](https://github.com/KATT/raf-query/issues/128)) ([667b38f](https://github.com/KATT/raf-query/commit/667b38ff5728dad3fdad6d90445aea480cfbd063))
36
+ - explain how a route loads its data on the homepage ([#121](https://github.com/KATT/raf-query/issues/121)) ([ec1d6ef](https://github.com/KATT/raf-query/commit/ec1d6efdad75ea2d05dd56790e5e980756afea13))
37
+ - frame the cache as async data, not server-state-only ([#126](https://github.com/KATT/raf-query/issues/126)) ([db9ba02](https://github.com/KATT/raf-query/commit/db9ba02995aa01cadacb2739fd258de3c920d440))
9
38
 
10
39
  ### Features
11
40
 
12
- * **examples:** reinstall shadcn on Base UI without Radix ([#125](https://github.com/KATT/raf-query/issues/125)) ([7c13ef3](https://github.com/KATT/raf-query/commit/7c13ef379dc297edbbe160516b26916d465a48d1))
41
+ - **examples:** reinstall shadcn on Base UI without Radix ([#125](https://github.com/KATT/raf-query/issues/125)) ([7c13ef3](https://github.com/KATT/raf-query/commit/7c13ef379dc297edbbe160516b26916d465a48d1))
13
42
 
14
43
  ## [0.7.11](https://github.com/KATT/raf-query/compare/0.7.10...0.7.11) (2026-10-14)
15
44
 
package/README.md CHANGED
@@ -4,21 +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";
23
+ import { infiniteQueryOptions, queryOptions } from "@raf-query/react";
22
24
 
23
25
  export function postQuery(input: { slug: string }) {
24
26
  return queryOptions({
@@ -27,20 +29,96 @@ export function postQuery(input: { slug: string }) {
27
29
  });
28
30
  }
29
31
 
30
- // 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
+
31
93
  export const Route = createFileRoute("/posts/$slug")({
32
- loader: async (args) => ({
33
- post: await args.context.queryClient.ensure(postQuery, {
34
- slug: args.params.slug,
35
- }),
36
- }),
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
+ },
37
102
  component: PostPage,
38
103
  });
39
104
 
40
- // …and the component reads it.
41
105
  function PostPage() {
42
- const [post] = useQuery(postQuery, Route.useLoaderData().post);
43
- return <h1>{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
+ );
44
122
  }
45
123
  ```
46
124
 
package/dist/client.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { DataOf, DehydrateOptions, DehydratedState, EnsuredQueryHandle, HydrateOptions, InputArgs, InputOf, LoadOptions, PagedQueryFactory, Query, QueryCache, QueryFactory, QueryHandle } from "@raf-query/core";
1
+ import { DataOf, DehydrateOptions, DehydratedState, HydrateOptions, InputArgs, InputOf, LoadOptions, PagedQueryFactory, Query, QueryCache, QueryFactory, QueryHandle, QueryHandleWithData } from "@raf-query/core";
2
2
  import { Effect, Layer } from "effect";
3
3
  //#region src/client.d.ts
4
4
  export interface QueryClientConfig<TServices = never> extends QueryCache.Config {
@@ -47,22 +47,21 @@ export declare class QueryClient<TServices = never> {
47
47
  */
48
48
  prefetch<TFactory extends QueryFactory<TServices>>(factory: TFactory, ...args: InputArgs<TFactory>): QueryHandle<TFactory>;
49
49
  /**
50
- * Start a query and wait for it on a cache miss. Rejects if that request
51
- * fails. Stale cached data resolves immediately and revalidates in the
52
- * browser. The handle is marked `ensured`, so `useQuery` returns its data
53
- * directly.
50
+ * {@link fetch} without waiting on stale data: start a query and wait for
51
+ * it on a cache miss. Stale cached data resolves immediately and
52
+ * revalidates in the browser. Rejects if that request fails.
54
53
  */
55
- ensure<TFactory extends QueryFactory<TServices>>(factory: TFactory, ...args: InputArgs<TFactory>): Promise<EnsuredQueryHandle<TFactory>>;
54
+ ensure<TFactory extends QueryFactory<TServices>>(factory: TFactory, ...args: InputArgs<TFactory>): Promise<QueryHandleWithData<TFactory>>;
56
55
  /** {@link ensure}, loading `options.pages` pages of a paged query. */
57
- ensure<TFactory extends PagedQueryFactory<TServices>>(factory: TFactory, input: InputOf<TFactory>, options: LoadOptions): Promise<EnsuredQueryHandle<TFactory>>;
56
+ ensure<TFactory extends PagedQueryFactory<TServices>>(factory: TFactory, input: InputOf<TFactory>, options: LoadOptions): Promise<QueryHandleWithData<TFactory>>;
58
57
  /**
59
- * Start a query and resolve to its data. Waits on a cache miss and on
60
- * stale data (on the server, only invalidated data), so the data is fresh.
61
- * Rejects if that request fails.
58
+ * Start a query and resolve to a handle holding its data. Waits on a cache
59
+ * miss and on stale data (on the server, only invalidated data), so the
60
+ * data is fresh. Rejects if that request fails.
62
61
  */
63
- fetch<TFactory extends QueryFactory<TServices>>(factory: TFactory, ...args: InputArgs<TFactory>): Promise<DataOf<TFactory>>;
62
+ fetch<TFactory extends QueryFactory<TServices>>(factory: TFactory, ...args: InputArgs<TFactory>): Promise<QueryHandleWithData<TFactory>>;
64
63
  /** {@link fetch}, loading `options.pages` pages of a paged query. */
65
- fetch<TFactory extends PagedQueryFactory<TServices>>(factory: TFactory, input: InputOf<TFactory>, options: LoadOptions): Promise<DataOf<TFactory>>;
64
+ fetch<TFactory extends PagedQueryFactory<TServices>>(factory: TFactory, input: InputOf<TFactory>, options: LoadOptions): Promise<QueryHandleWithData<TFactory>>;
66
65
  /**
67
66
  * Mark queries stale and refetch the ones being read. Resolves once those
68
67
  * refetches settle, so an action stays pending until fresh data is on
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, UseEnsuredQueryResult, 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 UseEnsuredQueryResult, 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,16 +1,17 @@
1
1
  import { QueryClient } from "./client.mjs";
2
- import { DehydrateOptions, DehydratedState, EnsuredQueryHandle, HydrateOptions, QueryHandle } from "@raf-query/core";
2
+ import { DehydrateOptions, DehydratedState, HydrateOptions, QueryHandle } from "@raf-query/core";
3
3
  import { AnyRouter } from "@tanstack/react-router";
4
4
  //#region src/tanstack-router-shared.d.ts
5
5
  declare module "@tanstack/react-router" {
6
6
  interface SerializableExtensions {
7
7
  /**
8
- * Handles are `{ key, input, ensured? }`, which the router's serializer
9
- * sends as is. Its type check can't see that on its own: keys are
8
+ * Handles are `{ key, input, data? }`, which the router's serializer
9
+ * sends as is (`data` is dehydrated anyway, so it is serializable). Its
10
+ * type check can't see that on its own: keys are
10
11
  * `ReadonlyArray<unknown>`, and the handle type names its factory.
11
12
  * So `beforeLoad` can put handles in route context.
12
13
  */
13
- rafQueryHandle: EnsuredQueryHandle | QueryHandle;
14
+ rafQueryHandle: QueryHandle;
14
15
  }
15
16
  }
16
17
  export interface RouterIntegrationOptions<TRouter extends AnyRouter> {
@@ -1,4 +1,4 @@
1
- import { DataOf, EnsuredQueryHandle, 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,8 +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`. */
67
- export type UseEnsuredQueryResult<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
+ /** {@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>];
68
68
  /**
69
69
  * Read the query a handle points at. Returns a promise to unwrap with
70
70
  * `use()` and the query's controls:
@@ -89,19 +89,25 @@ export type UseEnsuredQueryResult<TFactory extends QueryFactory, TThrow extends
89
89
  * The component re-renders when the promise changes, and when a control it
90
90
  * has read changes; changes to controls it never reads are skipped.
91
91
  *
92
- * A handle from `ensure` had its data loaded before the route rendered, so
93
- * the hook returns the data (or `Result`) itself:
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
94
  *
95
95
  * ```tsx
96
96
  * const [post, postState] = useQuery(postQuery, loaderData.post);
97
97
  * ```
98
98
  *
99
- * If that entry has lost its data since (garbage-collected, `reset`), the
100
- * hook suspends until it is refetched.
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.
101
107
  */
102
- export declare function useQuery<TFactory extends QueryFactory, const TThrow extends boolean | undefined = undefined>(factory: TFactory, handle: EnsuredQueryHandle<TFactory>, options?: UseQueryOptions & UseQueryThrowOptions<TFactory> & {
108
+ export declare function useQuery<TFactory extends QueryFactory, const TThrow extends boolean | undefined = undefined>(factory: TFactory, handle: QueryHandleWithData<TFactory>, options?: UseQueryOptions & UseQueryThrowOptions<TFactory> & {
103
109
  readonly throwOnError?: TThrow;
104
- }): UseEnsuredQueryResult<TFactory, TThrow>;
110
+ }): UseQueryResultWithData<TFactory, TThrow>;
105
111
  export declare function useQuery<TFactory extends QueryFactory, const TThrow extends boolean | undefined = undefined>(factory: TFactory, handle: QueryHandle<TFactory>, options?: UseQueryOptions & UseQueryThrowOptions<TFactory> & {
106
112
  readonly throwOnError?: TThrow;
107
113
  }): UseQueryResult<TFactory, TThrow>;
@@ -87,12 +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)) {
90
+ const throwOnError = options.throwOnError ?? client.throwOnError;
91
+ const withData = "data" in handle;
92
+ if (!throwOnError) {
91
93
  const resultPromise = asResultThenable(result.promise);
92
- if (handle.ensured === true) return [use(resultPromise), controls];
94
+ if (withData) return [use(resultPromise), controls];
93
95
  return [resultPromise, controls];
94
96
  }
95
- if (handle.ensured === true) return [use(result.promise), controls];
97
+ if (withData) return [use(result.promise), controls];
96
98
  return [result.promise, controls];
97
99
  }
98
100
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raf-query/react",
3
- "version": "0.7.12",
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.7.12"
49
+ "@raf-query/core": "0.9.0"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "@tanstack/react-router": "^1.170.0",