@raf-query/react 0.2.8 → 0.3.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,8 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.0](https://github.com/KATT/raf-query/compare/0.2.8...0.3.0) (2026-10-13)
4
+
5
+ ### ⚠ BREAKING CHANGES
6
+
7
+ * RSC apps hydrate only through HydrationBoundary; QueryProvider moves to /rsc ([#61](https://github.com/KATT/raf-query/issues/61))
8
+
9
+ ### Features
10
+
11
+ * 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)
12
+
3
13
  ## [0.2.8](https://github.com/KATT/raf-query/compare/0.2.7...0.2.8) (2026-10-13)
4
14
 
5
- * **codesandbox:** speed up the branch sync ([#60](https://github.com/KATT/raf-query/issues/60)) ([9099cb2](https://github.com/KATT/raf-query/commit/9099cb2ce937104d5feb8c7033233e701f412a28))
15
+ - **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
16
 
7
17
  ## [0.2.7](https://github.com/KATT/raf-query/compare/0.2.6...0.2.7) (2026-10-13)
8
18
 
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 passing server-component queries to the client |
39
- | `@raf-query/react/next` | `QueryProvider` for Next.js App Router |
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
 
@@ -435,7 +434,7 @@ Like `prefetch` in a loader, `useWaterfall` refetches stale cached data in the b
435
434
 
436
435
  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
436
 
438
- During SSR the `queryFn` runs on the server. TanStack Start server functions work there. Next.js rejects server actions called while rendering, so in Next.js start the dependent query in the server component instead:
437
+ 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
438
 
440
439
  ```tsx
441
440
  const post = await qc.fetch(postQuery, { slug });
@@ -518,7 +517,7 @@ export function getRouter() {
518
517
 
519
518
  `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
519
 
521
- ### Next.js App Router
520
+ ### React Server Components (Next.js App Router)
522
521
 
523
522
  ```tsx
524
523
  // app/get-query-client.ts
@@ -530,7 +529,7 @@ export const getQueryClient = cache(() => new QueryClient());
530
529
 
531
530
  ```tsx
532
531
  // app/layout.tsx
533
- import { QueryProvider } from "@raf-query/react/next";
532
+ import { QueryProvider } from "@raf-query/react/rsc";
534
533
 
535
534
  export default function RootLayout(props: { children: React.ReactNode }) {
536
535
  return (
@@ -570,8 +569,9 @@ export default async function Page(props: {
570
569
  ```
571
570
 
572
571
  - `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. The browser takes the server's data when it's newer than its own. Otherwise it keeps its copy and refetches it if stale. That covers a payload the router replays from its cache (back and forward) and a `prefetch` still pending on the server.
574
- - `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.
572
+ - `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.
573
+ - 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.
574
+ - `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
575
  - Server components that only render data can `await qc.fetch(...)` anywhere; the per-request client makes one request per query, whatever its `staleTime`.
576
576
  - `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
577
  - 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 a client's entries that already have a way to the browser
5
- * cache: sent by an earlier `HydrationBoundary` of the request, or, in the
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);
@@ -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 query = queryFor(useQueryClient(), factory, handle.input);
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;
@@ -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 });
@@ -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 query = useQueryClient().getQuery(factory, ...args);
26
- const promise = Query.readUnsafe(query);
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.2.8",
3
+ "version": "0.3.0",
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.2.8"
50
+ "@raf-query/core": "0.3.0"
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 };