@raf-query/core 0.0.12 → 0.0.13

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,8 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.0.13](https://github.com/KATT/raf-query/compare/0.0.12...0.0.13) (2026-10-12)
4
+
5
+ ### Features
6
+
7
+ * useQuery returns data for handles from ensure ([#16](https://github.com/KATT/raf-query/issues/16)) ([a50b8fd](https://github.com/KATT/raf-query/commit/a50b8fdc51b952341377d4b55f25ebb6b21d39e1))
8
+
3
9
  ## [0.0.12](https://github.com/KATT/raf-query/compare/0.0.11...0.0.12) (2026-10-12)
4
10
 
5
- * **examples:** move the shared db and ui packages into examples/.shared ([#14](https://github.com/KATT/raf-query/issues/14)) ([fccdc7b](https://github.com/KATT/raf-query/commit/fccdc7bb8075ce2fb0cdbe07b26b36f2ace2bd9c))
11
+ - **examples:** move the shared db and ui packages into examples/.shared ([#14](https://github.com/KATT/raf-query/issues/14)) ([fccdc7b](https://github.com/KATT/raf-query/commit/fccdc7bb8075ce2fb0cdbe07b26b36f2ace2bd9c))
6
12
 
7
13
  ## [0.0.11](https://github.com/KATT/raf-query/compare/0.0.10...0.0.11) (2026-10-12)
8
14
 
package/README.md CHANGED
@@ -72,11 +72,11 @@ The factory is the only place a query is declared paged. Every other API — `pr
72
72
 
73
73
  All three verbs live on the query client and accept a factory plus its input.
74
74
 
75
- | Call | Returns | Blocks? | Use for |
76
- | --------------------------------- | ---------------------- | ------- | ---------------------------------------------------------- |
77
- | `qc.prefetch(factory, input)` | `QueryHandle` | No | Data the route can render without; it streams in |
78
- | `await qc.ensure(factory, input)` | `Promise<QueryHandle>` | Yes | Data the route must not render without |
79
- | `await qc.fetch(factory, input)` | `Promise<Data>` | Yes | Rendering in a server component, or building another input |
75
+ | Call | Returns | Blocks? | Use for |
76
+ | --------------------------------- | ----------------------------- | ------- | ---------------------------------------------------------------------- |
77
+ | `qc.prefetch(factory, input)` | `QueryHandle` | No | Data the route can render without; it streams in |
78
+ | `await qc.ensure(factory, input)` | `Promise<EnsuredQueryHandle>` | Yes | Data the route must not render without; `useQuery` returns it directly |
79
+ | `await qc.fetch(factory, input)` | `Promise<Data>` | Yes | Rendering in a server component, or building another input |
80
80
 
81
81
  ```tsx
82
82
  // TanStack Start
@@ -125,9 +125,16 @@ interface QueryHandle<TFactory> {
125
125
  readonly key: QueryKey; // ["post", "hello"]
126
126
  readonly input: InputOf<TFactory>; // { slug: "hello" }
127
127
  }
128
+
129
+ // From `ensure`: the route waited for the data.
130
+ interface EnsuredQueryHandle<TFactory> {
131
+ readonly key: QueryKey;
132
+ readonly input: InputOf<TFactory>;
133
+ readonly ensured: true;
134
+ }
128
135
  ```
129
136
 
130
- 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 a handle from `ensure` versus `prefetch` shows whether the route waited for it.
137
+ 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.
131
138
 
132
139
  ## Reading queries
133
140
 
@@ -135,17 +142,20 @@ Handles hold no data and no functions. Data reaches the client cache through the
135
142
 
136
143
  ```ts
137
144
  const [promise, controls] = useQuery(factory, handle, options?);
145
+ const [data, controls] = useQuery(factory, ensuredHandle, options?);
138
146
  ```
139
147
 
140
148
  `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.
141
149
 
142
150
  `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.
143
151
 
152
+ 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.
153
+
144
154
  ```tsx
145
155
  function PostPage() {
146
156
  const loaderData = Route.useLoaderData();
147
- const [postPromise, postState] = useQuery(postQuery, loaderData.post);
148
- const post = use(postPromise);
157
+ // `post` came from `qc.ensure`, `comments` from `qc.prefetch`.
158
+ const [post, postState] = useQuery(postQuery, loaderData.post);
149
159
 
150
160
  return (
151
161
  <article>
@@ -267,6 +277,7 @@ Either way, an input that's already cached renders immediately. A component that
267
277
  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:
268
278
 
269
279
  ```tsx
280
+ // This loader prefetches the post instead of ensuring it.
270
281
  const [postPromise, postState] = useQuery(postQuery, loaderData.post);
271
282
 
272
283
  return (
@@ -282,10 +293,10 @@ In Next.js the same rules apply: read `searchParams` in the page, `prefetch` wit
282
293
 
283
294
  ### Passing promises down
284
295
 
285
- A component can call the hook and hand the promise to a child, so only the child suspends:
296
+ A component can call the hook with a prefetched handle and hand the promise to a child, so only the child suspends:
286
297
 
287
298
  ```tsx
288
- const [postPromise] = useQuery(postQuery, loaderData.post);
299
+ const [postPromise] = useQuery(postQuery, loaderData.post); // from `qc.prefetch`
289
300
  return (
290
301
  <Suspense fallback={<Spinner />}>
291
302
  <PostBody post={postPromise} />
@@ -435,6 +446,7 @@ export default async function Page(props: {
435
446
  - `HydrationBoundary` sends the request client's entries to the browser cache; pending ones stream as promises. Render one in each server component that starts queries for client components. Entries already sent in this request are skipped.
436
447
  - `QueryProvider` creates the browser client, and streams queries that client components start during SSR (`useWaterfall`) into the HTML. Pass `config` for the client's options; it must be serializable.
437
448
  - Server components that only render data can `await qc.fetch(...)` anywhere; the per-request client dedupes them.
449
+ - `fetch` returns data, not a handle. When a server component renders the data and 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.
438
450
  - 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.
439
451
 
440
452
  #### Server-only queries
@@ -555,6 +567,7 @@ Query options, accepted by `queryOptions`, `infiniteQueryOptions` and `config.de
555
567
  | `hashKey(key)`, `matchesKey(key, prefix)` | Key helpers |
556
568
  | `CancelledError` | Rejection of a request cancelled by `clear()` or a restart |
557
569
  | `QueryHandle<TFactory>` | Handle type for props |
570
+ | `EnsuredQueryHandle<TFactory>` | Handle type for props fed by `ensure` |
558
571
  | `DataOf<TFactory>`, `InputOf<TFactory>` | A factory's data and input types |
559
572
 
560
573
  `@raf-query/react` (plus all of the above):
package/dist/client.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { QueryKey } from "./key.mjs";
2
- import { DataOf, InputArgs, InputOf, LoadOptions, QueryBehavior, QueryFactory, QueryHandle } from "./options.mjs";
2
+ import { DataOf, EnsuredQueryHandle, InputArgs, InputOf, LoadOptions, QueryBehavior, QueryFactory, QueryHandle } from "./options.mjs";
3
3
  import { Query, QueryState } from "./query.mjs";
4
4
  import { Effect, Stream } from "effect";
5
5
  //#region src/client.d.ts
@@ -36,7 +36,7 @@ export type LoadArgs<TFactory extends QueryFactory> = InputArgs<TFactory> | [inp
36
36
  export type Updater<TData> = TData | ((previous: TData | undefined) => TData);
37
37
  /** Effect-returning twins of the client's async methods. */
38
38
  export interface QueryClientEffect {
39
- ensure<TFactory extends QueryFactory>(factory: TFactory, ...args: LoadArgs<TFactory>): Effect.Effect<QueryHandle<TFactory>, unknown>;
39
+ ensure<TFactory extends QueryFactory>(factory: TFactory, ...args: LoadArgs<TFactory>): Effect.Effect<EnsuredQueryHandle<TFactory>, unknown>;
40
40
  fetch<TFactory extends QueryFactory>(factory: TFactory, ...args: LoadArgs<TFactory>): Effect.Effect<DataOf<TFactory>, unknown>;
41
41
  invalidate<TFactory extends QueryFactory>(factory: TFactory, ...args: InputArgs<TFactory>): Effect.Effect<void>;
42
42
  invalidate(filter: QueryFilter): Effect.Effect<void>;
@@ -69,9 +69,10 @@ export declare class QueryClient {
69
69
  prefetch<TFactory extends QueryFactory>(factory: TFactory, ...args: InputArgs<TFactory>): QueryHandle<TFactory>;
70
70
  /**
71
71
  * Start a query and wait for it on a cache miss. Rejects if that request
72
- * fails. Stale cached data resolves immediately and revalidates.
72
+ * fails. Stale cached data resolves immediately and revalidates. The
73
+ * handle is marked `ensured`, so `useQuery` returns its data directly.
73
74
  */
74
- ensure<TFactory extends QueryFactory>(factory: TFactory, ...args: LoadArgs<TFactory>): Promise<QueryHandle<TFactory>>;
75
+ ensure<TFactory extends QueryFactory>(factory: TFactory, ...args: LoadArgs<TFactory>): Promise<EnsuredQueryHandle<TFactory>>;
75
76
  /** Like `ensure`, but resolves to the data. */
76
77
  fetch<TFactory extends QueryFactory>(factory: TFactory, ...args: LoadArgs<TFactory>): Promise<DataOf<TFactory>>;
77
78
  /**
package/dist/client.mjs CHANGED
@@ -21,6 +21,12 @@ function typed(entry) {
21
21
  function handleOf(entry) {
22
22
  return entry.handle;
23
23
  }
24
+ function ensuredHandleOf(entry) {
25
+ return {
26
+ ...handleOf(entry),
27
+ ensured: true
28
+ };
29
+ }
24
30
  /**
25
31
  * The query cache. Create one per request on the server and one per browser
26
32
  * tab.
@@ -95,12 +101,13 @@ var QueryClient = class {
95
101
  }
96
102
  /**
97
103
  * Start a query and wait for it on a cache miss. Rejects if that request
98
- * fails. Stale cached data resolves immediately and revalidates.
104
+ * fails. Stale cached data resolves immediately and revalidates. The
105
+ * handle is marked `ensured`, so `useQuery` returns its data directly.
99
106
  */
100
107
  async ensure(factory, ...args) {
101
108
  const entry = this.#build(factory, args[0]);
102
109
  await this.#load(entry, args[1]?.pages);
103
- return handleOf(entry);
110
+ return ensuredHandleOf(entry);
104
111
  }
105
112
  /** Like `ensure`, but resolves to the data. */
106
113
  async fetch(factory, ...args) {
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Settled, allKeyed } from "./all-keyed.mjs";
2
2
  import { QueryKey, hashKey, matchesKey } from "./key.mjs";
3
- import { AnyQueryOptions, DataOf, InfiniteData, InfiniteQueryOptions, InputArgs, InputOf, IsPaged, LoadOptions, PageFunctionContext, PageOf, QueryBehavior, QueryFactory, QueryFnResult, QueryFunctionContext, QueryHandle, QueryOptions, RetryPolicy, infiniteQueryOptions, queryOptions } from "./options.mjs";
3
+ import { AnyQueryOptions, DataOf, EnsuredQueryHandle, InfiniteData, InfiniteQueryOptions, InputArgs, InputOf, IsPaged, LoadOptions, PageFunctionContext, PageOf, QueryBehavior, QueryFactory, QueryFnResult, QueryFunctionContext, QueryHandle, QueryOptions, RetryPolicy, infiniteQueryOptions, queryOptions } from "./options.mjs";
4
4
  import { CancelledError, FetchRequest, Query, QueryObserver, QueryState } from "./query.mjs";
5
5
  import { CacheEvent, LoadArgs, QueryClient, QueryClientConfig, QueryClientEffect, QueryFilter, Updater } from "./client.mjs";
6
6
  import { DehydrateOptions, DehydratedQuery, DehydratedState, HydrateOptions, canDehydrate, dehydrate, dehydrateQuery, hydrate } from "./hydration.mjs";
7
- export { type AnyQueryOptions, type CacheEvent, CancelledError, type DataOf, type DehydrateOptions, type DehydratedQuery, type DehydratedState, type FetchRequest, type HydrateOptions, type InfiniteData, type InfiniteQueryOptions, type InputArgs, type InputOf, type IsPaged, type LoadArgs, type LoadOptions, type PageFunctionContext, type PageOf, type Query, type QueryBehavior, QueryClient, type QueryClientConfig, type QueryClientEffect, type QueryFactory, type QueryFilter, type QueryFnResult, type QueryFunctionContext, type QueryHandle, type QueryKey, type QueryObserver, type QueryOptions, type QueryState, type RetryPolicy, type Settled, type Updater, allKeyed, canDehydrate, dehydrate, dehydrateQuery, hashKey, hydrate, infiniteQueryOptions, matchesKey, queryOptions };
7
+ export { type AnyQueryOptions, type CacheEvent, CancelledError, type DataOf, type DehydrateOptions, type DehydratedQuery, type DehydratedState, type EnsuredQueryHandle, type FetchRequest, type HydrateOptions, type InfiniteData, type InfiniteQueryOptions, type InputArgs, type InputOf, type IsPaged, type LoadArgs, type LoadOptions, type PageFunctionContext, type PageOf, type Query, type QueryBehavior, QueryClient, type QueryClientConfig, type QueryClientEffect, type QueryFactory, type QueryFilter, type QueryFnResult, type QueryFunctionContext, type QueryHandle, type QueryKey, type QueryObserver, type QueryOptions, type QueryState, type RetryPolicy, type Settled, type Updater, allKeyed, canDehydrate, dehydrate, dehydrateQuery, hashKey, hydrate, infiniteQueryOptions, matchesKey, queryOptions };
@@ -103,15 +103,27 @@ export type LoadOptions<TFactory extends QueryFactory> = IsPaged<TFactory> exten
103
103
  readonly pages?: number;
104
104
  } : never;
105
105
  declare const factoryType: unique symbol;
106
- /**
107
- * The serializable reference a loader or server component hands to the
108
- * component that reads the query. It holds no data and no functions.
109
- */
110
- export interface QueryHandle<TFactory extends QueryFactory = QueryFactory> {
106
+ interface HandleFields<TFactory extends QueryFactory> {
111
107
  /** Type-only: ties the handle to its factory. Never present at runtime. */
112
108
  readonly [factoryType]?: TFactory;
113
109
  /** The factory's input; `unknown` on a handle of no particular factory. */
114
110
  readonly input: QueryFactory extends TFactory ? unknown : InputOf<TFactory>;
115
111
  readonly key: QueryKey;
116
112
  }
113
+ /**
114
+ * The serializable reference a loader or server component hands to the
115
+ * component that reads the query. It holds no data and no functions.
116
+ */
117
+ export interface QueryHandle<TFactory extends QueryFactory = QueryFactory> extends HandleFields<TFactory> {
118
+ readonly ensured?: false;
119
+ }
120
+ /**
121
+ * A handle from `ensure`: its data was loaded before the handle was
122
+ * returned, so `useQuery` returns the data rather than a promise. Not
123
+ * assignable to {@link QueryHandle}, so a prop typed for one can't receive
124
+ * the other.
125
+ */
126
+ export interface EnsuredQueryHandle<TFactory extends QueryFactory = QueryFactory> extends HandleFields<TFactory> {
127
+ readonly ensured: true;
128
+ }
117
129
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raf-query/core",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "description": "Framework-agnostic core of raf-query, a render-as-you-fetch query library",
5
5
  "keywords": [
6
6
  "query",