@raf-query/react 0.11.1 → 0.12.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/README.md +32 -59
  3. package/dist/bind.d.mts +19 -0
  4. package/dist/bind.mjs +21 -0
  5. package/dist/browser-render.mjs +46 -0
  6. package/dist/devtools.d.mts +24 -2
  7. package/dist/devtools.mjs +236 -1
  8. package/dist/hydration-boundary/hydrate.mjs +10 -13
  9. package/dist/hydration-boundary/hydration-boundary-react-server.d.mts +4 -0
  10. package/dist/hydration-boundary/hydration-boundary-react-server.mjs +18 -9
  11. package/dist/hydration-boundary/hydration-boundary.d.mts +9 -29
  12. package/dist/hydration-boundary/hydration-boundary.mjs +6 -27
  13. package/dist/index-react-server.d.mts +7 -6
  14. package/dist/index-react-server.mjs +4 -6
  15. package/dist/index-shared.d.mts +5 -6
  16. package/dist/index-shared.mjs +3 -6
  17. package/dist/index.d.mts +7 -6
  18. package/dist/index.mjs +5 -4
  19. package/dist/provider.d.mts +4 -4
  20. package/dist/provider.mjs +5 -8
  21. package/dist/react-server-hooks.d.mts +17 -0
  22. package/dist/react-server-hooks.mjs +19 -0
  23. package/dist/tanstack-router-shared.d.mts +4 -7
  24. package/dist/tanstack-router-shared.mjs +12 -22
  25. package/dist/tanstack-router.d.mts +2 -2
  26. package/dist/tanstack-router.mjs +10 -14
  27. package/dist/tracked.d.mts +1 -0
  28. package/dist/tracked.mjs +65 -0
  29. package/dist/use-infinite-query.d.mts +27 -0
  30. package/dist/use-infinite-query.mjs +35 -0
  31. package/dist/use-is-fetching.d.mts +6 -16
  32. package/dist/use-is-fetching.mjs +11 -31
  33. package/dist/use-query.d.mts +43 -54
  34. package/dist/use-query.mjs +102 -47
  35. package/package.json +10 -9
  36. package/dist/devtools-panel.d.mts +0 -38
  37. package/dist/devtools-panel.mjs +0 -896
  38. package/dist/devtools-store.mjs +0 -104
  39. package/dist/hydration-boundary/sent.mjs +0 -16
  40. package/dist/query-provider/browser-requests.mjs +0 -38
  41. package/dist/query-provider/query-provider.d.mts +0 -21
  42. package/dist/query-provider/query-provider.mjs +0 -33
  43. package/dist/use-waterfall.d.mts +0 -18
  44. package/dist/use-waterfall.mjs +0 -41
package/CHANGELOG.md CHANGED
@@ -1,10 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.12.0](https://github.com/KATT/raf-query/compare/0.11.1...0.12.0) (2026-11-08)
4
+
5
+ ### ⚠ BREAKING CHANGES
6
+
7
+ * greenfield rewrite on a promise kernel, with load() handles and opt-in behaviors ([#230](https://github.com/KATT/raf-query/issues/230))
8
+
9
+ ### Features
10
+
11
+ * greenfield rewrite on a promise kernel, with load() handles and opt-in behaviors ([#230](https://github.com/KATT/raf-query/issues/230)) ([e2a6f85](https://github.com/KATT/raf-query/commit/e2a6f85297992dc8a5cdd3f871a54452393ae978))
12
+
3
13
  ## [0.11.1](https://github.com/KATT/raf-query/compare/0.11.0...0.11.1) (2026-10-17)
4
14
 
5
15
  ### Features
6
16
 
7
- * **core:** take throwOnError from the query's options (2/2) ([#214](https://github.com/KATT/raf-query/issues/214)) ([ef1f0fd](https://github.com/KATT/raf-query/commit/ef1f0fd8eda2a73f4c4c268e8a3a5cf91a0ba0d8)), references [#448](https://github.com/KATT/raf-query/issues/448)
17
+ - **core:** take throwOnError from the query's options (2/2) ([#214](https://github.com/KATT/raf-query/issues/214)) ([ef1f0fd](https://github.com/KATT/raf-query/commit/ef1f0fd8eda2a73f4c4c268e8a3a5cf91a0ba0d8)), references [#448](https://github.com/KATT/raf-query/issues/448)
8
18
 
9
19
  ## [0.11.0](https://github.com/KATT/raf-query/compare/0.10.4...0.11.0) (2026-10-16)
10
20
 
package/README.md CHANGED
@@ -10,28 +10,31 @@ Render-as-you-fetch data fetching for React: start queries in route loaders and
10
10
  ## Install
11
11
 
12
12
  ```bash
13
- npm install @raf-query/react effect
13
+ npm install @raf-query/react
14
14
  ```
15
15
 
16
- Apps never import `effect` unless they write Effect code. Requires React 19.2 or newer.
16
+ Requires React 19.2 or newer.
17
17
 
18
18
  ## Usage
19
19
 
20
20
  A query is a factory. The loader and the component call the same one. A paged list, such as comments, uses `infiniteQueryOptions`.
21
21
 
22
22
  ```tsx
23
- import { infiniteQueryOptions, queryOptions } from "@raf-query/react";
23
+ import { group, infiniteQueryOptions, queryOptions } from "@raf-query/react";
24
+
25
+ const posts = group("posts");
26
+ const comments = group(posts, "comments");
24
27
 
25
28
  export function postQuery(input: { slug: string }) {
26
29
  return queryOptions({
27
- queryKey: ["post", input.slug],
30
+ queryKey: posts("bySlug", input.slug),
28
31
  queryFn: () => getPost(input.slug),
29
32
  });
30
33
  }
31
34
 
32
35
  export function commentsQuery(input: { slug: string }) {
33
36
  return infiniteQueryOptions({
34
- queryKey: ["comments", input.slug],
37
+ queryKey: comments(input.slug),
35
38
  queryFn: (context) =>
36
39
  listComments({ cursor: context.pageParam, slug: input.slug }),
37
40
  initialPageParam: 0,
@@ -40,28 +43,36 @@ export function commentsQuery(input: { slug: string }) {
40
43
  }
41
44
  ```
42
45
 
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.
46
+ A loader names its queries in one `load` call. **`prefetch`** starts a request without waiting for it: the page renders now, and `use()` suspends whatever is still loading under its Suspense fallback. **`swr`** holds the navigation until the data is cached, so `use()` returns it without suspending. `useQuery` returns a thenable for every handle, because the cache can drop a query between the loader and the render.
44
47
 
45
48
  ```tsx
46
- import { type QueryHandle, useQuery } from "@raf-query/react";
49
+ import {
50
+ load,
51
+ prefetch,
52
+ type QueryHandle,
53
+ swr,
54
+ useInfiniteQuery,
55
+ useQuery,
56
+ } from "@raf-query/react";
47
57
  import { Suspense, use } from "react";
48
58
 
49
59
  export const Route = createFileRoute("/posts/$slug")({
50
- loader: (args) => {
51
- const qc = args.context.queryClient;
60
+ loader: async (args) => {
52
61
  const { slug } = args.params;
53
- return {
54
- comments: qc.prefetch(commentsQuery, { slug }),
55
- post: qc.prefetch(postQuery, { slug }),
56
- };
62
+ const [handles] = await load(args.context.queryClient, {
63
+ post: swr(postQuery, { slug }),
64
+ comments: prefetch(commentsQuery, { slug }),
65
+ });
66
+ return handles;
57
67
  },
58
68
  component: PostPage,
59
69
  });
60
70
 
61
71
  function PostPage() {
62
72
  const data = Route.useLoaderData();
63
- const [postPromise] = useQuery(postQuery, data.post);
64
- const post = use(postPromise);
73
+ const [postThenable] = useQuery(postQuery, data.post);
74
+ // The loader waited for it, so this doesn't suspend.
75
+ const post = use(postThenable);
65
76
  return (
66
77
  <article>
67
78
  <h1>{post.title}</h1>
@@ -73,8 +84,8 @@ function PostPage() {
73
84
  }
74
85
 
75
86
  function Comments(props: { handle: QueryHandle<typeof commentsQuery> }) {
76
- const [pagesPromise] = useQuery(commentsQuery, props.handle);
77
- const pages = use(pagesPromise);
87
+ const [pagesThenable] = useInfiniteQuery(commentsQuery, props.handle);
88
+ const pages = use(pagesThenable);
78
89
  return pages.pages
79
90
  .flatMap((page) => page.items)
80
91
  .map((comment) => (
@@ -85,50 +96,12 @@ function Comments(props: { handle: QueryHandle<typeof commentsQuery> }) {
85
96
  }
86
97
  ```
87
98
 
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
-
93
- export const Route = createFileRoute("/posts/$slug")({
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
- },
102
- component: PostPage,
103
- });
104
-
105
- function PostPage() {
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
- );
122
- }
123
- ```
124
-
125
99
  ## Entry points
126
100
 
127
- | Import | Use |
128
- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
129
- | `@raf-query/react` | `QueryClientProvider`, `QueryProvider`, `useQuery`, `useWaterfall`, `useIsFetching`, `HydrationBoundary`, and all of core, `QueryClient` included |
130
- | `@raf-query/react/tanstack-router` | [TanStack Start and Router](https://raf-query.katt.dev/docs/tanstack-start) SSR streaming |
131
- | `@raf-query/react/devtools` | The [devtools](https://raf-query.katt.dev/docs/devtools) panel; omit the element to tree-shake it |
101
+ | Import | Use |
102
+ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
103
+ | `@raf-query/react` | `QueryClientProvider`, `useQuery`, `useInfiniteQuery`, `useIsFetching`, `HydrationBoundary`, `QueryDevtools`, and all of core (`group`, `dehydrate`, `hydrate`); what an app doesn't import stays out of its bundle |
104
+ | `@raf-query/react/tanstack-router` | [TanStack Start and Router](https://raf-query.katt.dev/docs/tanstack-start) SSR streaming |
132
105
 
133
106
  ## License
134
107
 
@@ -0,0 +1,19 @@
1
+ import { QueryClient } from "@raf-query/core";
2
+ import { RootOf } from "@raf-query/core/internal";
3
+ //#region src/bind.d.ts
4
+ /**
5
+ * `useQueryClient` typed with a root, so verbs called with the client it
6
+ * returns check factories and targets against that root. `T` is the
7
+ * client's type, or the type of a function that makes it. Bind once per app:
8
+ *
9
+ * ```ts
10
+ * // raf.ts
11
+ * export const useQueryClient = bindUseQueryClient<typeof makeQueryClient>();
12
+ * ```
13
+ *
14
+ * It returns `useQueryClient` itself; only the types change. It isn't a
15
+ * hook, so call it at module level. This module has no `"use client"`, so a
16
+ * server module can import the file that binds it.
17
+ */
18
+ export declare function bindUseQueryClient<T>(): () => QueryClient<RootOf<T>>;
19
+ //#endregion
package/dist/bind.mjs ADDED
@@ -0,0 +1,21 @@
1
+ import { useQueryClient } from "./provider.mjs";
2
+ //#region src/bind.ts
3
+ /**
4
+ * `useQueryClient` typed with a root, so verbs called with the client it
5
+ * returns check factories and targets against that root. `T` is the
6
+ * client's type, or the type of a function that makes it. Bind once per app:
7
+ *
8
+ * ```ts
9
+ * // raf.ts
10
+ * export const useQueryClient = bindUseQueryClient<typeof makeQueryClient>();
11
+ * ```
12
+ *
13
+ * It returns `useQueryClient` itself; only the types change. It isn't a
14
+ * hook, so call it at module level. This module has no `"use client"`, so a
15
+ * server module can import the file that binds it.
16
+ */
17
+ function bindUseQueryClient() {
18
+ return useQueryClient;
19
+ }
20
+ //#endregion
21
+ export { bindUseQueryClient };
@@ -0,0 +1,46 @@
1
+ import { rejected } from "@raf-query/core/internal";
2
+ //#region src/browser-render.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
+ /**
9
+ * What a server render's read rejects with when nothing on the server will
10
+ * settle the query. `use()` throws it, React renders the nearest Suspense
11
+ * fallback, and the browser renders the content.
12
+ */
13
+ var BrowserRenderError = class extends Error {
14
+ digest = clientRendering;
15
+ name = "BrowserRenderError";
16
+ };
17
+ /** Server clients a server components' `HydrationBoundary` feeds. */
18
+ const fedByServerComponents = /* @__PURE__ */ new WeakSet();
19
+ /**
20
+ * Mark a server render's client as fed by server components: from now on,
21
+ * its renders leave a query nothing started to the browser instead of
22
+ * fetching it, since nothing would send that request's data down.
23
+ */
24
+ function markFedByServerComponents(client) {
25
+ if (client.isServer) fedByServerComponents.add(client);
26
+ }
27
+ /** Whether a render on `client` leaves a query nothing started to the browser. */
28
+ function leavesUnstartedToBrowser(client) {
29
+ return fedByServerComponents.has(client);
30
+ }
31
+ const reads = /* @__PURE__ */ new WeakMap();
32
+ /**
33
+ * The read of an entry a server render won't settle: no data, no error and
34
+ * no request in flight. It rejects, and the boundary that `use()`s it renders
35
+ * in the browser.
36
+ */
37
+ function browserRender(entry) {
38
+ let read = reads.get(entry);
39
+ if (!read) {
40
+ read = rejected(new BrowserRenderError(`Query ${entry.hash} loads in the browser: nothing started it on the server`));
41
+ reads.set(entry, read);
42
+ }
43
+ return read;
44
+ }
45
+ //#endregion
46
+ export { browserRender, leavesUnstartedToBrowser, markFedByServerComponents };
@@ -1,2 +1,24 @@
1
- import { DevtoolsTheme, QueryDevtools, QueryDevtoolsPanel, QueryDevtoolsPanelProps, QueryDevtoolsProps } from "./devtools-panel.mjs";
2
- export { type DevtoolsTheme, QueryDevtools, QueryDevtoolsPanel, type QueryDevtoolsPanelProps, type QueryDevtoolsProps };
1
+ import { QueryClient } from "@raf-query/core";
2
+ import { CSSProperties } from "react";
3
+ //#region src/devtools.d.ts
4
+ export interface QueryDevtoolsPanelProps {
5
+ /** The client to inspect. Default: the nearest `QueryClientProvider`'s. */
6
+ readonly client?: QueryClient;
7
+ /** Shows a close button that calls this. */
8
+ readonly onClose?: () => void;
9
+ /** Styles for the panel's root element, e.g. its height. */
10
+ readonly style?: CSSProperties;
11
+ }
12
+ export interface QueryDevtoolsProps {
13
+ /** Corner of the toggle button. Default: `"bottom-right"`. */
14
+ readonly buttonPosition?: "bottom-left" | "bottom-right" | "top-left" | "top-right";
15
+ /** The client to inspect. Default: the nearest `QueryClientProvider`'s. */
16
+ readonly client?: QueryClient;
17
+ /** Open on the first render. */
18
+ readonly initialIsOpen?: boolean;
19
+ }
20
+ /** The devtools panel, to place in your own layout. */
21
+ export declare function QueryDevtoolsPanel(props: QueryDevtoolsPanelProps): import("react").JSX.Element;
22
+ /** A toggle button that opens the devtools panel over the page. */
23
+ export declare function QueryDevtools(props: QueryDevtoolsProps): import("react").JSX.Element;
24
+ //#endregion
package/dist/devtools.mjs CHANGED
@@ -1,3 +1,238 @@
1
1
  "use client";
2
- import { QueryDevtools, QueryDevtoolsPanel } from "./devtools-panel.mjs";
2
+ import { useOptionalQueryClient } from "./provider.mjs";
3
+ import { fetchEntry, loadsOf, remove, reset } from "@raf-query/core/internal";
4
+ import { useMemo, useState, useSyncExternalStore } from "react";
5
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+ //#region src/devtools.tsx
7
+ function statusOf(entry) {
8
+ const state = entry.state;
9
+ if (state.isFetching) return "fetching";
10
+ if (state.status === "error") return "error";
11
+ return state.hasData ? entry.readers.size > 0 ? "active" : "inactive" : "pending";
12
+ }
13
+ function snapshotOf(client) {
14
+ return {
15
+ loads: loadsOf(client),
16
+ rows: [...client["~entries"].values()].map((entry) => ({
17
+ entry,
18
+ readers: entry.readers.size,
19
+ state: entry.state,
20
+ status: statusOf(entry),
21
+ updatedAt: entry.state.updatedAt
22
+ })).toSorted((a, b) => a.entry.hash.localeCompare(b.entry.hash))
23
+ };
24
+ }
25
+ function snapshotStore(client) {
26
+ let changed = false;
27
+ let snapshot = snapshotOf(client);
28
+ return {
29
+ read: () => {
30
+ if (changed) {
31
+ changed = false;
32
+ snapshot = snapshotOf(client);
33
+ }
34
+ return snapshot;
35
+ },
36
+ subscribe: (onChange) => {
37
+ changed = true;
38
+ return client.subscribe(() => {
39
+ changed = true;
40
+ onChange();
41
+ });
42
+ }
43
+ };
44
+ }
45
+ function useSnapshot(client) {
46
+ const store = useMemo(() => snapshotStore(client), [client]);
47
+ return useSyncExternalStore(store.subscribe, store.read, store.read);
48
+ }
49
+ const colors = {
50
+ active: "#16a34a",
51
+ error: "#dc2626",
52
+ fetching: "#2563eb",
53
+ inactive: "#6b7280",
54
+ pending: "#ca8a04"
55
+ };
56
+ function Panel(props) {
57
+ const snapshot = useSnapshot(props.client);
58
+ const [selected, setSelected] = useState();
59
+ const row = snapshot.rows.find((candidate) => candidate.entry.hash === selected);
60
+ return /* @__PURE__ */ jsxs("div", {
61
+ style: {
62
+ background: "#0b0d12",
63
+ color: "#e5e7eb",
64
+ display: "grid",
65
+ font: "12px/1.4 ui-monospace, monospace",
66
+ gridTemplateColumns: "minmax(0, 1fr) minmax(0, 1fr)",
67
+ height: 320,
68
+ ...props.style
69
+ },
70
+ children: [/* @__PURE__ */ jsxs("div", {
71
+ style: {
72
+ borderRight: "1px solid #1f2937",
73
+ overflow: "auto"
74
+ },
75
+ children: [
76
+ /* @__PURE__ */ jsxs("div", {
77
+ style: {
78
+ display: "flex",
79
+ gap: 8,
80
+ padding: 8
81
+ },
82
+ children: [/* @__PURE__ */ jsxs("strong", { children: [
83
+ "Queries (",
84
+ snapshot.rows.length,
85
+ ")"
86
+ ] }), props.onClose ? /* @__PURE__ */ jsx("button", {
87
+ onClick: props.onClose,
88
+ style: { marginLeft: "auto" },
89
+ type: "button",
90
+ children: "Close"
91
+ }) : null]
92
+ }),
93
+ snapshot.rows.map((item) => /* @__PURE__ */ jsxs("button", {
94
+ onClick: () => setSelected(item.entry.hash),
95
+ style: {
96
+ background: item.entry.hash === selected ? "#1f2937" : "transparent",
97
+ border: 0,
98
+ color: "inherit",
99
+ display: "flex",
100
+ font: "inherit",
101
+ gap: 8,
102
+ padding: "4px 8px",
103
+ textAlign: "left",
104
+ width: "100%"
105
+ },
106
+ type: "button",
107
+ children: [
108
+ /* @__PURE__ */ jsx("span", {
109
+ style: {
110
+ color: colors[item.status],
111
+ minWidth: 64
112
+ },
113
+ children: item.status
114
+ }),
115
+ /* @__PURE__ */ jsx("span", {
116
+ style: { minWidth: 16 },
117
+ children: item.readers
118
+ }),
119
+ /* @__PURE__ */ jsx("span", {
120
+ style: {
121
+ overflow: "hidden",
122
+ textOverflow: "ellipsis"
123
+ },
124
+ children: item.entry.hash
125
+ })
126
+ ]
127
+ }, item.entry.hash)),
128
+ /* @__PURE__ */ jsxs("div", {
129
+ style: {
130
+ borderTop: "1px solid #1f2937",
131
+ padding: 8
132
+ },
133
+ children: [/* @__PURE__ */ jsx("strong", { children: "Loads" }), snapshot.loads.toReversed().map((record) => /* @__PURE__ */ jsx("div", { children: record.entries.map((one) => `${one.name} (${one.kind})`).join(", ") }, record.startedAt + record.entries.map((one) => one.hash).join()))]
134
+ })
135
+ ]
136
+ }), /* @__PURE__ */ jsx("div", {
137
+ style: {
138
+ overflow: "auto",
139
+ padding: 8
140
+ },
141
+ children: row ? /* @__PURE__ */ jsxs(Fragment, { children: [
142
+ /* @__PURE__ */ jsxs("div", {
143
+ style: {
144
+ display: "flex",
145
+ gap: 8,
146
+ marginBottom: 8
147
+ },
148
+ children: [
149
+ /* @__PURE__ */ jsx("button", {
150
+ onClick: () => void fetchEntry(row.entry, { restart: true }),
151
+ type: "button",
152
+ children: "Refetch"
153
+ }),
154
+ /* @__PURE__ */ jsx("button", {
155
+ onClick: () => void reset(row.entry),
156
+ type: "button",
157
+ children: "Reset"
158
+ }),
159
+ /* @__PURE__ */ jsx("button", {
160
+ onClick: () => remove(row.entry),
161
+ type: "button",
162
+ children: "Remove"
163
+ })
164
+ ]
165
+ }),
166
+ /* @__PURE__ */ jsxs("div", { children: [
167
+ "Updated:",
168
+ " ",
169
+ row.updatedAt ? new Date(row.updatedAt).toLocaleTimeString() : "never"
170
+ ] }),
171
+ /* @__PURE__ */ jsx("pre", {
172
+ style: { whiteSpace: "pre-wrap" },
173
+ children: JSON.stringify(row.state.error ?? row.state.data, null, 2)
174
+ })
175
+ ] }) : /* @__PURE__ */ jsx("div", {
176
+ style: { color: "#6b7280" },
177
+ children: "Select a query"
178
+ })
179
+ })]
180
+ });
181
+ }
182
+ /** The devtools panel, to place in your own layout. */
183
+ function QueryDevtoolsPanel(props) {
184
+ const nearest = useOptionalQueryClient();
185
+ const client = props.client ?? nearest;
186
+ if (!client) throw new Error("QueryDevtoolsPanel: pass `client`, or render it in a QueryClientProvider");
187
+ return /* @__PURE__ */ jsx(Panel, {
188
+ ...props,
189
+ client
190
+ });
191
+ }
192
+ const corners = {
193
+ "bottom-left": {
194
+ bottom: 12,
195
+ left: 12
196
+ },
197
+ "bottom-right": {
198
+ bottom: 12,
199
+ right: 12
200
+ },
201
+ "top-left": {
202
+ left: 12,
203
+ top: 12
204
+ },
205
+ "top-right": {
206
+ right: 12,
207
+ top: 12
208
+ }
209
+ };
210
+ /** A toggle button that opens the devtools panel over the page. */
211
+ function QueryDevtools(props) {
212
+ const [open, setOpen] = useState(props.initialIsOpen ?? false);
213
+ const corner = corners[props.buttonPosition ?? "bottom-right"];
214
+ return /* @__PURE__ */ jsx(Fragment, { children: open ? /* @__PURE__ */ jsx("div", {
215
+ style: {
216
+ bottom: 0,
217
+ left: 0,
218
+ position: "fixed",
219
+ right: 0,
220
+ zIndex: 99999
221
+ },
222
+ children: /* @__PURE__ */ jsx(QueryDevtoolsPanel, {
223
+ client: props.client,
224
+ onClose: () => setOpen(false)
225
+ })
226
+ }) : /* @__PURE__ */ jsx("button", {
227
+ onClick: () => setOpen(true),
228
+ style: {
229
+ ...corner,
230
+ position: "fixed",
231
+ zIndex: 99999
232
+ },
233
+ type: "button",
234
+ children: "raf-query"
235
+ }) });
236
+ }
237
+ //#endregion
3
238
  export { QueryDevtools, QueryDevtoolsPanel };
@@ -1,6 +1,8 @@
1
1
  "use client";
2
2
  import { useQueryClient } from "../provider.mjs";
3
- import { Query, hashKey, hydrate } from "@raf-query/core";
3
+ import { markFedByServerComponents } from "../browser-render.mjs";
4
+ import { hydrate } from "@raf-query/core";
5
+ import { hashKey } from "@raf-query/core/internal";
4
6
  import { useEffect, useState } from "react";
5
7
  //#region src/hydration-boundary/hydrate.tsx
6
8
  /**
@@ -8,11 +10,10 @@ import { useEffect, useState } from "react";
8
10
  * render, and entries it already shows, which must wait for a commit.
9
11
  */
10
12
  function split(client, state) {
11
- const shown = new Set(client.getAll().filter((query) => Query.getUnsafe(query).hasData).map((query) => query.hash));
12
- const isShown = (query) => shown.has(hashKey(query.key));
13
+ const isShown = (sent) => client["~entries"].get(hashKey(sent.queryKey))?.state.hasData === true;
13
14
  return {
14
- deferred: { queries: state.queries.filter(isShown) },
15
- now: { queries: state.queries.filter((query) => !isShown(query)) }
15
+ deferred: { entries: state.entries.filter(isShown) },
16
+ now: { entries: state.entries.filter((sent) => !isShown(sent)) }
16
17
  };
17
18
  }
18
19
  /**
@@ -25,12 +26,12 @@ function split(client, state) {
25
26
  */
26
27
  function Hydrate(props) {
27
28
  const client = useQueryClient();
29
+ if (props.fromServerComponents) markFedByServerComponents(client);
28
30
  const state = props.state;
29
- const options = props.options;
30
31
  const [hydrated, setHydrated] = useState();
31
32
  if (hydrated?.state !== state) {
32
33
  const parts = split(client, state);
33
- hydrate(client, parts.now, options);
34
+ hydrate(client, parts.now);
34
35
  setHydrated({
35
36
  deferred: parts.deferred,
36
37
  state
@@ -38,12 +39,8 @@ function Hydrate(props) {
38
39
  }
39
40
  const deferred = hydrated?.deferred;
40
41
  useEffect(() => {
41
- if (deferred && deferred.queries.length > 0) hydrate(client, deferred, options);
42
- }, [
43
- client,
44
- deferred,
45
- options
46
- ]);
42
+ if (deferred && deferred.entries.length > 0) hydrate(client, deferred);
43
+ }, [client, deferred]);
47
44
  return props.children;
48
45
  }
49
46
  //#endregion
@@ -6,6 +6,10 @@ import { HydrationBoundaryProps } from "./hydration-boundary.mjs";
6
6
  * browser cache: pending entries cross as promises and stream in as they
7
7
  * settle, and entries an earlier boundary already sent in this request are
8
8
  * skipped. With `state`, writes that state, as the browser version does.
9
+ *
10
+ * It also marks the client components' server render: from there on, a read
11
+ * of a query nothing sent renders its Suspense fallback on the server and
12
+ * loads in the browser, instead of fetching where nothing would send it.
9
13
  */
10
14
  export declare function HydrationBoundary(props: HydrationBoundaryProps): import("react").JSX.Element;
11
15
  //#endregion
@@ -1,27 +1,36 @@
1
1
  import { Hydrate } from "./hydrate.mjs";
2
- import { sentHashes } from "./sent.mjs";
3
2
  import { dehydrate } from "@raf-query/core";
4
3
  import { jsx } from "react/jsx-runtime";
5
4
  //#region src/hydration-boundary/hydration-boundary-react-server.tsx
5
+ const sentByClient = /* @__PURE__ */ new WeakMap();
6
+ /**
7
+ * Hashes of the request client's entries an earlier `HydrationBoundary`
8
+ * already sent, so each entry crosses once per request.
9
+ */
10
+ function sentHashes(client) {
11
+ const sent = sentByClient.get(client) ?? /* @__PURE__ */ new Set();
12
+ sentByClient.set(client, sent);
13
+ return sent;
14
+ }
6
15
  /**
7
16
  * `HydrationBoundary` in server components (the `react-server` export
8
17
  * condition). With `client`, sends the request client's entries to the
9
18
  * browser cache: pending entries cross as promises and stream in as they
10
19
  * settle, and entries an earlier boundary already sent in this request are
11
20
  * skipped. With `state`, writes that state, as the browser version does.
21
+ *
22
+ * It also marks the client components' server render: from there on, a read
23
+ * of a query nothing sent renders its Suspense fallback on the server and
24
+ * loads in the browser, instead of fetching where nothing would send it.
12
25
  */
13
26
  function HydrationBoundary(props) {
14
- if (props.client === void 0) return /* @__PURE__ */ jsx(Hydrate, {
15
- options: props.options,
16
- state: props.state,
17
- children: props.children
18
- });
19
27
  const client = props.client;
20
- const state = dehydrate(client, {
21
- sent: sentHashes(client),
22
- ...props.options
28
+ const state = client === void 0 ? props.state : dehydrate(client, {
29
+ ...props.options,
30
+ sent: sentHashes(client)
23
31
  });
24
32
  return /* @__PURE__ */ jsx(Hydrate, {
33
+ fromServerComponents: true,
25
34
  state,
26
35
  children: props.children
27
36
  });