@raf-query/react 0.2.8 → 0.3.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 +15 -1
- package/README.md +83 -15
- package/dist/browser-requests.mjs +38 -0
- package/dist/hydrate.mjs +0 -7
- package/dist/query-provider.d.mts +21 -0
- package/dist/query-provider.mjs +33 -0
- package/dist/rsc.d.mts +3 -1
- package/dist/rsc.mjs +3 -2
- package/dist/sent.mjs +2 -4
- package/dist/use-query.mjs +4 -1
- package/dist/use-waterfall.d.mts +3 -1
- package/dist/use-waterfall.mjs +8 -3
- package/package.json +2 -8
- package/dist/next.d.mts +0 -25
- package/dist/next.mjs +0 -62
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.1](https://github.com/KATT/raf-query/compare/0.3.0...0.3.1) (2026-10-13)
|
|
4
|
+
|
|
5
|
+
* teach avoiding dependent queries; frame prefetch vs ensure as loading UX ([#62](https://github.com/KATT/raf-query/issues/62)) ([ec62760](https://github.com/KATT/raf-query/commit/ec627609cbbee9fa4ff0cf568f69964341e27daa))
|
|
6
|
+
|
|
7
|
+
## [0.3.0](https://github.com/KATT/raf-query/compare/0.2.8...0.3.0) (2026-10-13)
|
|
8
|
+
|
|
9
|
+
### ⚠ BREAKING CHANGES
|
|
10
|
+
|
|
11
|
+
- RSC apps hydrate only through HydrationBoundary; QueryProvider moves to /rsc ([#61](https://github.com/KATT/raf-query/issues/61))
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
- RSC apps hydrate only through HydrationBoundary; QueryProvider moves to /rsc ([#61](https://github.com/KATT/raf-query/issues/61)) ([772de79](https://github.com/KATT/raf-query/commit/772de796adfdf267a9184fd75624ff30ae53f4fc)), references [#59](https://github.com/KATT/raf-query/issues/59) [#59](https://github.com/KATT/raf-query/issues/59) [#59](https://github.com/KATT/raf-query/issues/59) [#59](https://github.com/KATT/raf-query/issues/59)
|
|
16
|
+
|
|
3
17
|
## [0.2.8](https://github.com/KATT/raf-query/compare/0.2.7...0.2.8) (2026-10-13)
|
|
4
18
|
|
|
5
|
-
|
|
19
|
+
- **codesandbox:** speed up the branch sync ([#60](https://github.com/KATT/raf-query/issues/60)) ([9099cb2](https://github.com/KATT/raf-query/commit/9099cb2ce937104d5feb8c7033233e701f412a28))
|
|
6
20
|
|
|
7
21
|
## [0.2.7](https://github.com/KATT/raf-query/compare/0.2.6...0.2.7) (2026-10-13)
|
|
8
22
|
|
package/README.md
CHANGED
|
@@ -32,12 +32,11 @@ The same issue tracker, built on two frameworks so the integrations can be compa
|
|
|
32
32
|
|
|
33
33
|
`@raf-query/react` subpath entries for framework integrations and tooling:
|
|
34
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` | `HydrationBoundary` for
|
|
39
|
-
| `@raf-query/react/
|
|
40
|
-
| `@raf-query/react/devtools` | [Devtools](#devtools) panel for inspecting the cache in development |
|
|
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 |
|
|
41
40
|
|
|
42
41
|
## Principles
|
|
43
42
|
|
|
@@ -125,7 +124,7 @@ All three verbs live on the query client and accept a factory plus its input.
|
|
|
125
124
|
| --------------------------------- | ----------------------------- | ------- | ---------------------------------------------------------------------- |
|
|
126
125
|
| `qc.prefetch(factory, input)` | `QueryHandle` | No | Data the route can render without; it streams in |
|
|
127
126
|
| `await qc.ensure(factory, input)` | `Promise<EnsuredQueryHandle>` | Yes | Data the route must not render without; `useQuery` returns it directly |
|
|
128
|
-
| `await qc.fetch(factory, input)` | `Promise<Data>` | Yes | Rendering in a server component
|
|
127
|
+
| `await qc.fetch(factory, input)` | `Promise<Data>` | Yes | Rendering data in a server component |
|
|
129
128
|
|
|
130
129
|
```tsx
|
|
131
130
|
// TanStack Start
|
|
@@ -140,6 +139,13 @@ export const Route = createFileRoute("/posts/$slug")({
|
|
|
140
139
|
});
|
|
141
140
|
```
|
|
142
141
|
|
|
142
|
+
`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:
|
|
143
|
+
|
|
144
|
+
- **`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.
|
|
145
|
+
- **`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.
|
|
146
|
+
|
|
147
|
+
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.
|
|
148
|
+
|
|
143
149
|
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.
|
|
144
150
|
|
|
145
151
|
```ts
|
|
@@ -151,13 +157,74 @@ return await allKeyed({
|
|
|
151
157
|
});
|
|
152
158
|
```
|
|
153
159
|
|
|
154
|
-
|
|
160
|
+
### Avoid dependent queries
|
|
161
|
+
|
|
162
|
+
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:
|
|
163
|
+
|
|
164
|
+
- **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.
|
|
165
|
+
- **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`).
|
|
166
|
+
|
|
167
|
+
<details>
|
|
168
|
+
<summary>Example: the current user's organization</summary>
|
|
169
|
+
|
|
170
|
+
A settings page needs the user and their organization, and the organization's id is a field of the user:
|
|
155
171
|
|
|
156
172
|
```ts
|
|
157
|
-
const
|
|
158
|
-
|
|
173
|
+
export const Route = createFileRoute("/settings")({
|
|
174
|
+
loader: async (args) => {
|
|
175
|
+
const qc = args.context.queryClient;
|
|
176
|
+
const user = await qc.fetch(currentUserQuery);
|
|
177
|
+
return {
|
|
178
|
+
user: qc.prefetch(currentUserQuery),
|
|
179
|
+
org: qc.prefetch(orgQuery, { orgId: user.orgId }),
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
});
|
|
159
183
|
```
|
|
160
184
|
|
|
185
|
+
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.
|
|
186
|
+
|
|
187
|
+
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.
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
export function currentOrgQuery() {
|
|
191
|
+
return queryOptions({
|
|
192
|
+
queryKey: ["org", "current"],
|
|
193
|
+
queryFn: () => getCurrentOrg(),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export const Route = createFileRoute("/settings")({
|
|
198
|
+
loader: (args) => {
|
|
199
|
+
const qc = args.context.queryClient;
|
|
200
|
+
return {
|
|
201
|
+
user: qc.prefetch(currentUserQuery),
|
|
202
|
+
org: qc.prefetch(currentOrgQuery),
|
|
203
|
+
};
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
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.
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
export const Route = createFileRoute("/orgs/$orgId/settings")({
|
|
212
|
+
loader: (args) => {
|
|
213
|
+
const qc = args.context.queryClient;
|
|
214
|
+
return {
|
|
215
|
+
user: qc.prefetch(currentUserQuery),
|
|
216
|
+
org: qc.prefetch(orgQuery, { orgId: args.params.orgId }),
|
|
217
|
+
};
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
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.
|
|
223
|
+
|
|
224
|
+
</details>
|
|
225
|
+
|
|
226
|
+
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.
|
|
227
|
+
|
|
161
228
|
Behavior:
|
|
162
229
|
|
|
163
230
|
- Every verb dedupes against the cache. A fresh entry is reused; a stale entry is refetched in the background.
|
|
@@ -435,7 +502,7 @@ Like `prefetch` in a loader, `useWaterfall` refetches stale cached data in the b
|
|
|
435
502
|
|
|
436
503
|
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.
|
|
437
504
|
|
|
438
|
-
|
|
505
|
+
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:
|
|
439
506
|
|
|
440
507
|
```tsx
|
|
441
508
|
const post = await qc.fetch(postQuery, { slug });
|
|
@@ -518,7 +585,7 @@ export function getRouter() {
|
|
|
518
585
|
|
|
519
586
|
`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.
|
|
520
587
|
|
|
521
|
-
### Next.js App Router
|
|
588
|
+
### React Server Components (Next.js App Router)
|
|
522
589
|
|
|
523
590
|
```tsx
|
|
524
591
|
// app/get-query-client.ts
|
|
@@ -530,7 +597,7 @@ export const getQueryClient = cache(() => new QueryClient());
|
|
|
530
597
|
|
|
531
598
|
```tsx
|
|
532
599
|
// app/layout.tsx
|
|
533
|
-
import { QueryProvider } from "@raf-query/react/
|
|
600
|
+
import { QueryProvider } from "@raf-query/react/rsc";
|
|
534
601
|
|
|
535
602
|
export default function RootLayout(props: { children: React.ReactNode }) {
|
|
536
603
|
return (
|
|
@@ -570,8 +637,9 @@ export default async function Page(props: {
|
|
|
570
637
|
```
|
|
571
638
|
|
|
572
639
|
- `getQueryClient` is scoped to one request by React's `cache()`.
|
|
573
|
-
- `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.
|
|
574
|
-
- `
|
|
640
|
+
- `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.
|
|
641
|
+
- 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.
|
|
642
|
+
- `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.
|
|
575
643
|
- Server components that only render data can `await qc.fetch(...)` anywhere; the per-request client makes one request per query, whatever its `staleTime`.
|
|
576
644
|
- `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.
|
|
577
645
|
- 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.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Query } from "@raf-query/core";
|
|
2
|
+
//#region src/browser-requests.ts
|
|
3
|
+
/**
|
|
4
|
+
* The digest Next.js gives a boundary that renders only in the browser
|
|
5
|
+
* (`next/dynamic` with `ssr: false`); it doesn't report those errors.
|
|
6
|
+
*/
|
|
7
|
+
const clientRendering = "BAILOUT_TO_CLIENT_SIDE_RENDERING";
|
|
8
|
+
const browserOnly = /* @__PURE__ */ new WeakSet();
|
|
9
|
+
/**
|
|
10
|
+
* Make a server client's render start no request: it reads what a
|
|
11
|
+
* `HydrationBoundary` sent, and anything else loads in the browser.
|
|
12
|
+
*/
|
|
13
|
+
function requestInBrowserOnly(client) {
|
|
14
|
+
browserOnly.add(client);
|
|
15
|
+
}
|
|
16
|
+
/** Whether a render may start `client`'s requests (`useWaterfall`). */
|
|
17
|
+
function startsRequests(client) {
|
|
18
|
+
return !(client.isServer && browserOnly.has(client));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Thrown by a server render's read of a query no `HydrationBoundary` sent.
|
|
22
|
+
* React renders the nearest Suspense fallback, and the browser renders the
|
|
23
|
+
* content.
|
|
24
|
+
*/
|
|
25
|
+
var BrowserRenderError = class extends Error {
|
|
26
|
+
digest = clientRendering;
|
|
27
|
+
constructor(hash) {
|
|
28
|
+
super(`Query ${hash} loads in the browser: no HydrationBoundary sent it`);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
/** Throw {@link BrowserRenderError} if reading `query` would start a request here. */
|
|
32
|
+
function assertSent(client, query) {
|
|
33
|
+
if (startsRequests(client)) return;
|
|
34
|
+
const state = Query.getUnsafe(query);
|
|
35
|
+
if (!state.hasData && !state.isFetching && state.status !== "error") throw new BrowserRenderError(query.hash);
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { BrowserRenderError, assertSent, requestInBrowserOnly, startsRequests };
|
package/dist/hydrate.mjs
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { hydrate } from "./client.mjs";
|
|
3
3
|
import { useQueryClient } from "./provider.mjs";
|
|
4
|
-
import { sentHashes } from "./sent.mjs";
|
|
5
4
|
import { Query, hashKey } from "@raf-query/core";
|
|
6
5
|
import { useEffect, useState } from "react";
|
|
7
6
|
//#region src/hydrate.tsx
|
|
8
|
-
/** In the SSR pass, entries from the RSC payload needn't be streamed again. */
|
|
9
|
-
function markSent(client, state) {
|
|
10
|
-
const sent = sentHashes(client);
|
|
11
|
-
for (const query of state.queries) sent.add(hashKey(query.key));
|
|
12
|
-
}
|
|
13
7
|
/**
|
|
14
8
|
* Entries the client has no data for, which children need during this
|
|
15
9
|
* render, and entries it already shows, which must wait for a commit.
|
|
@@ -37,7 +31,6 @@ function Hydrate(props) {
|
|
|
37
31
|
const [hydrated, setHydrated] = useState();
|
|
38
32
|
if (hydrated?.state !== state) {
|
|
39
33
|
const parts = split(client, state);
|
|
40
|
-
markSent(client, state);
|
|
41
34
|
hydrate(client, parts.now, options);
|
|
42
35
|
setHydrated({
|
|
43
36
|
deferred: parts.deferred,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { QueryClientConfig } from "./client.mjs";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
//#region src/query-provider.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The query client provider for React Server Components apps. Creates one
|
|
6
|
+
* client per server render and one per browser tab. The server render
|
|
7
|
+
* starts no request: client components read what `HydrationBoundary` sent,
|
|
8
|
+
* and a query nothing sent renders its Suspense fallback there and loads in
|
|
9
|
+
* the browser.
|
|
10
|
+
*
|
|
11
|
+
* ```tsx
|
|
12
|
+
* // app/layout.tsx
|
|
13
|
+
* <QueryProvider>{props.children}</QueryProvider>
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare function QueryProvider(props: {
|
|
17
|
+
readonly children?: ReactNode;
|
|
18
|
+
/** Options for the client it creates. */
|
|
19
|
+
readonly config?: QueryClientConfig;
|
|
20
|
+
}): import("react").JSX.Element;
|
|
21
|
+
//#endregion
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { QueryClient } from "./client.mjs";
|
|
3
|
+
import { QueryClientProvider } from "./provider.mjs";
|
|
4
|
+
import { requestInBrowserOnly } from "./browser-requests.mjs";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
7
|
+
//#region src/query-provider.tsx
|
|
8
|
+
/**
|
|
9
|
+
* The query client provider for React Server Components apps. Creates one
|
|
10
|
+
* client per server render and one per browser tab. The server render
|
|
11
|
+
* starts no request: client components read what `HydrationBoundary` sent,
|
|
12
|
+
* and a query nothing sent renders its Suspense fallback there and loads in
|
|
13
|
+
* the browser.
|
|
14
|
+
*
|
|
15
|
+
* ```tsx
|
|
16
|
+
* // app/layout.tsx
|
|
17
|
+
* <QueryProvider>{props.children}</QueryProvider>
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
function QueryProvider(props) {
|
|
21
|
+
const config = props.config;
|
|
22
|
+
const [client] = useState(() => {
|
|
23
|
+
const created = new QueryClient(config);
|
|
24
|
+
requestInBrowserOnly(created);
|
|
25
|
+
return created;
|
|
26
|
+
});
|
|
27
|
+
return /* @__PURE__ */ jsx(QueryClientProvider, {
|
|
28
|
+
client,
|
|
29
|
+
children: props.children
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { QueryProvider };
|
package/dist/rsc.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { QueryClient } from "./client.mjs";
|
|
2
|
+
import { QueryProvider } from "./query-provider.mjs";
|
|
2
3
|
import { DehydrateOptions } from "@raf-query/core";
|
|
3
4
|
import { ReactNode } from "react";
|
|
4
5
|
//#region src/rsc.d.ts
|
|
@@ -23,4 +24,5 @@ export declare function HydrationBoundary(props: {
|
|
|
23
24
|
readonly client: QueryClient;
|
|
24
25
|
readonly options?: DehydrateOptions;
|
|
25
26
|
}): import("react").JSX.Element;
|
|
26
|
-
//#endregion
|
|
27
|
+
//#endregion
|
|
28
|
+
export { QueryProvider };
|
package/dist/rsc.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { dehydrate } from "./client.mjs";
|
|
2
|
-
import { sentHashes } from "./sent.mjs";
|
|
3
2
|
import { Hydrate } from "./hydrate.mjs";
|
|
3
|
+
import { sentHashes } from "./sent.mjs";
|
|
4
|
+
import { QueryProvider } from "./query-provider.mjs";
|
|
4
5
|
import { jsx } from "react/jsx-runtime";
|
|
5
6
|
//#region src/rsc.tsx
|
|
6
7
|
/**
|
|
@@ -30,4 +31,4 @@ function HydrationBoundary(props) {
|
|
|
30
31
|
});
|
|
31
32
|
}
|
|
32
33
|
//#endregion
|
|
33
|
-
export { HydrationBoundary };
|
|
34
|
+
export { HydrationBoundary, QueryProvider };
|
package/dist/sent.mjs
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
//#region src/sent.ts
|
|
2
2
|
const sentByClient = /* @__PURE__ */ new WeakMap();
|
|
3
3
|
/**
|
|
4
|
-
* Hashes of
|
|
5
|
-
*
|
|
6
|
-
* SSR pass, hydrated from the RSC payload or streamed by `QueryProvider`.
|
|
7
|
-
* Integrations pass it as `sent`, so each entry crosses once.
|
|
4
|
+
* Hashes of the request client's entries an earlier `HydrationBoundary`
|
|
5
|
+
* already sent. Boundaries pass it as `sent`, so each entry crosses once.
|
|
8
6
|
*/
|
|
9
7
|
function sentHashes(client) {
|
|
10
8
|
let sent = sentByClient.get(client);
|
package/dist/use-query.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { unsafeCast } from "./cast.mjs";
|
|
3
3
|
import { useQueryClient } from "./provider.mjs";
|
|
4
|
+
import { assertSent } from "./browser-requests.mjs";
|
|
4
5
|
import { Query, QueryObserver } from "@raf-query/core";
|
|
5
6
|
import { Effect } from "effect";
|
|
6
7
|
import { use, useEffect, useState, useSyncExternalStore } from "react";
|
|
@@ -35,7 +36,9 @@ function storeFor(query, options) {
|
|
|
35
36
|
};
|
|
36
37
|
}
|
|
37
38
|
function useQuery(factory, handle, options = {}) {
|
|
38
|
-
const
|
|
39
|
+
const client = useQueryClient();
|
|
40
|
+
const query = queryFor(client, factory, handle.input);
|
|
41
|
+
assertSent(client, query);
|
|
39
42
|
Query.readUnsafe(query);
|
|
40
43
|
const refetchInterval = options.refetchInterval;
|
|
41
44
|
const refetchOnReconnect = options.refetchOnReconnect;
|
package/dist/use-waterfall.d.mts
CHANGED
|
@@ -5,7 +5,9 @@ import { InputArgs, QueryFactory, QueryHandle } from "@raf-query/core";
|
|
|
5
5
|
* at render time (an id from a parent query's data). Each call is a request
|
|
6
6
|
* waterfall: prefer starting the query in a loader or server component.
|
|
7
7
|
* Like `prefetch` in a loader, it refetches stale cached data in the
|
|
8
|
-
* background, once per mount and input.
|
|
8
|
+
* background, once per mount and input. Under the `/rsc` `QueryProvider` it
|
|
9
|
+
* starts nothing on the server: the reader's Suspense boundary renders its
|
|
10
|
+
* fallback there, and the query loads in the browser.
|
|
9
11
|
*
|
|
10
12
|
* ```tsx
|
|
11
13
|
* const author = useWaterfall(authorQuery, { id: post.authorId });
|
package/dist/use-waterfall.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useQueryClient } from "./provider.mjs";
|
|
3
|
+
import { startsRequests } from "./browser-requests.mjs";
|
|
3
4
|
import { Query } from "@raf-query/core";
|
|
4
5
|
import { useState } from "react";
|
|
5
6
|
//#region src/use-waterfall.ts
|
|
@@ -14,7 +15,9 @@ const waitedFor = /* @__PURE__ */ new WeakSet();
|
|
|
14
15
|
* at render time (an id from a parent query's data). Each call is a request
|
|
15
16
|
* waterfall: prefer starting the query in a loader or server component.
|
|
16
17
|
* Like `prefetch` in a loader, it refetches stale cached data in the
|
|
17
|
-
* background, once per mount and input.
|
|
18
|
+
* background, once per mount and input. Under the `/rsc` `QueryProvider` it
|
|
19
|
+
* starts nothing on the server: the reader's Suspense boundary renders its
|
|
20
|
+
* fallback there, and the query loads in the browser.
|
|
18
21
|
*
|
|
19
22
|
* ```tsx
|
|
20
23
|
* const author = useWaterfall(authorQuery, { id: post.authorId });
|
|
@@ -22,9 +25,11 @@ const waitedFor = /* @__PURE__ */ new WeakSet();
|
|
|
22
25
|
* ```
|
|
23
26
|
*/
|
|
24
27
|
function useWaterfall(factory, ...args) {
|
|
25
|
-
const
|
|
26
|
-
const
|
|
28
|
+
const client = useQueryClient();
|
|
29
|
+
const query = client.getQuery(factory, ...args);
|
|
27
30
|
const [started, setStarted] = useState();
|
|
31
|
+
if (!startsRequests(client)) return query.handle;
|
|
32
|
+
const promise = Query.readUnsafe(query);
|
|
28
33
|
if (started !== query.hash) {
|
|
29
34
|
if (!Query.getUnsafe(query).hasData) waitedFor.add(promise);
|
|
30
35
|
else if (!waitedFor.delete(promise)) Query.revalidateUnsafe(query);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raf-query/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "React bindings for raf-query, a render-as-you-fetch query library",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"query",
|
|
@@ -35,7 +35,6 @@
|
|
|
35
35
|
".": "./dist/index.mjs",
|
|
36
36
|
"./devtools": "./dist/devtools.mjs",
|
|
37
37
|
"./devtools/production": "./dist/devtools-production.mjs",
|
|
38
|
-
"./next": "./dist/next.mjs",
|
|
39
38
|
"./rsc": "./dist/rsc.mjs",
|
|
40
39
|
"./tanstack-router": {
|
|
41
40
|
"workerd": "./dist/tanstack-router.mjs",
|
|
@@ -48,20 +47,16 @@
|
|
|
48
47
|
}
|
|
49
48
|
},
|
|
50
49
|
"dependencies": {
|
|
51
|
-
"@raf-query/core": "0.
|
|
50
|
+
"@raf-query/core": "0.3.1"
|
|
52
51
|
},
|
|
53
52
|
"peerDependencies": {
|
|
54
53
|
"@tanstack/react-router": "^1.170.0",
|
|
55
54
|
"effect": "^4.0.0",
|
|
56
|
-
"next": "^16.0.0",
|
|
57
55
|
"react": "^19.2.0"
|
|
58
56
|
},
|
|
59
57
|
"peerDependenciesMeta": {
|
|
60
58
|
"@tanstack/react-router": {
|
|
61
59
|
"optional": true
|
|
62
|
-
},
|
|
63
|
-
"next": {
|
|
64
|
-
"optional": true
|
|
65
60
|
}
|
|
66
61
|
},
|
|
67
62
|
"devDependencies": {
|
|
@@ -74,7 +69,6 @@
|
|
|
74
69
|
"@vitest/coverage-v8": "5.0.1",
|
|
75
70
|
"effect": "^4.0.1",
|
|
76
71
|
"happy-dom": "21.0.1",
|
|
77
|
-
"next": "16.4.0",
|
|
78
72
|
"react": "19.3.0",
|
|
79
73
|
"react-dom": "19.3.0",
|
|
80
74
|
"typescript": "7.0.2",
|
package/dist/next.d.mts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { QueryClientConfig } from "./client.mjs";
|
|
2
|
-
import { DehydratedState } from "@raf-query/core";
|
|
3
|
-
import { ReactNode } from "react";
|
|
4
|
-
//#region src/next.d.ts
|
|
5
|
-
declare global {
|
|
6
|
-
/** Entries streamed by `QueryProvider` during SSR, until the browser client takes over. */
|
|
7
|
-
var RAF_QUERY_SSR: {
|
|
8
|
-
push(state: DehydratedState): void;
|
|
9
|
-
} | undefined;
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* The query client provider for Next.js App Router. Creates one client per
|
|
13
|
-
* request during SSR and one per tab in the browser.
|
|
14
|
-
*
|
|
15
|
-
* ```tsx
|
|
16
|
-
* // app/layout.tsx
|
|
17
|
-
* <QueryProvider>{props.children}</QueryProvider>
|
|
18
|
-
* ```
|
|
19
|
-
*/
|
|
20
|
-
export declare function QueryProvider(props: {
|
|
21
|
-
readonly children?: ReactNode;
|
|
22
|
-
/** Options for the client it creates. */
|
|
23
|
-
readonly config?: QueryClientConfig;
|
|
24
|
-
}): import("react").JSX.Element;
|
|
25
|
-
//#endregion
|
package/dist/next.mjs
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { QueryClient, dehydrate, hydrate } from "./client.mjs";
|
|
3
|
-
import { QueryClientProvider } from "./provider.mjs";
|
|
4
|
-
import { sentHashes } from "./sent.mjs";
|
|
5
|
-
import { Query } from "@raf-query/core";
|
|
6
|
-
import { useState } from "react";
|
|
7
|
-
import { jsx } from "react/jsx-runtime";
|
|
8
|
-
import { useServerInsertedHTML } from "next/navigation";
|
|
9
|
-
//#region src/next.tsx
|
|
10
|
-
function escapeJson(json) {
|
|
11
|
-
return json.replaceAll("<", String.raw`\u003c`).replaceAll("\u2028", String.raw`\u2028`).replaceAll("\u2029", String.raw`\u2029`);
|
|
12
|
-
}
|
|
13
|
-
/** Hydrate what SSR streamed so far, then hydrate every later chunk on arrival. */
|
|
14
|
-
function receiveStream(client) {
|
|
15
|
-
const queued = globalThis.RAF_QUERY_SSR;
|
|
16
|
-
if (Array.isArray(queued)) for (const state of queued) hydrate(client, state);
|
|
17
|
-
globalThis.RAF_QUERY_SSR = { push: (state) => hydrate(client, state) };
|
|
18
|
-
}
|
|
19
|
-
function hasData(query) {
|
|
20
|
-
return Query.getUnsafe(query).hasData;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Streams entries that client components loaded during SSR (`useWaterfall`)
|
|
24
|
-
* into the HTML, ahead of the markup that reads them. Entries that arrived
|
|
25
|
-
* through `HydrationBoundary` are skipped: the RSC payload carries them.
|
|
26
|
-
* Data must be JSON-serializable, so only settled entries are sent.
|
|
27
|
-
*/
|
|
28
|
-
function useServerStream(client) {
|
|
29
|
-
useServerInsertedHTML(() => {
|
|
30
|
-
const state = dehydrate(client, {
|
|
31
|
-
filter: hasData,
|
|
32
|
-
sent: sentHashes(client)
|
|
33
|
-
});
|
|
34
|
-
if (state.queries.length === 0) return null;
|
|
35
|
-
const json = escapeJson(JSON.stringify(state));
|
|
36
|
-
return /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: `(self.RAF_QUERY_SSR=self.RAF_QUERY_SSR||[]).push(${json})` } });
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* The query client provider for Next.js App Router. Creates one client per
|
|
41
|
-
* request during SSR and one per tab in the browser.
|
|
42
|
-
*
|
|
43
|
-
* ```tsx
|
|
44
|
-
* // app/layout.tsx
|
|
45
|
-
* <QueryProvider>{props.children}</QueryProvider>
|
|
46
|
-
* ```
|
|
47
|
-
*/
|
|
48
|
-
function QueryProvider(props) {
|
|
49
|
-
const config = props.config;
|
|
50
|
-
const [client] = useState(() => {
|
|
51
|
-
const created = new QueryClient(config);
|
|
52
|
-
if (!created.isServer) receiveStream(created);
|
|
53
|
-
return created;
|
|
54
|
-
});
|
|
55
|
-
useServerStream(client);
|
|
56
|
-
return /* @__PURE__ */ jsx(QueryClientProvider, {
|
|
57
|
-
client,
|
|
58
|
-
children: props.children
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
//#endregion
|
|
62
|
-
export { QueryProvider };
|