@raf-query/react 0.11.1 → 0.13.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 (46) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/README.md +32 -59
  3. package/dist/bind.d.mts +21 -0
  4. package/dist/bind.mjs +23 -0
  5. package/dist/browser-render.mjs +46 -0
  6. package/dist/devtools.d.mts +42 -2
  7. package/dist/devtools.mjs +244 -1
  8. package/dist/hydration-boundary/hydrate.mjs +10 -13
  9. package/dist/hydration-boundary/hydration-boundary-react-server.d.mts +6 -0
  10. package/dist/hydration-boundary/hydration-boundary-react-server.mjs +20 -9
  11. package/dist/hydration-boundary/hydration-boundary.d.mts +15 -28
  12. package/dist/hydration-boundary/hydration-boundary.mjs +7 -26
  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 +11 -5
  20. package/dist/provider.mjs +12 -9
  21. package/dist/react-server-hooks.d.mts +43 -0
  22. package/dist/react-server-hooks.mjs +45 -0
  23. package/dist/tanstack-router-browser.d.mts +2 -0
  24. package/dist/tanstack-router-browser.mjs +2 -0
  25. package/dist/tanstack-router-shared.d.mts +9 -7
  26. package/dist/tanstack-router-shared.mjs +12 -22
  27. package/dist/tanstack-router.d.mts +4 -2
  28. package/dist/tanstack-router.mjs +12 -14
  29. package/dist/tracked.d.mts +1 -0
  30. package/dist/tracked.mjs +65 -0
  31. package/dist/use-infinite-query.d.mts +23 -0
  32. package/dist/use-infinite-query.mjs +30 -0
  33. package/dist/use-is-fetching.d.mts +7 -15
  34. package/dist/use-is-fetching.mjs +12 -30
  35. package/dist/use-query.d.mts +52 -50
  36. package/dist/use-query.mjs +112 -47
  37. package/package.json +10 -9
  38. package/dist/devtools-panel.d.mts +0 -38
  39. package/dist/devtools-panel.mjs +0 -896
  40. package/dist/devtools-store.mjs +0 -104
  41. package/dist/hydration-boundary/sent.mjs +0 -16
  42. package/dist/query-provider/browser-requests.mjs +0 -38
  43. package/dist/query-provider/query-provider.d.mts +0 -21
  44. package/dist/query-provider/query-provider.mjs +0 -33
  45. package/dist/use-waterfall.d.mts +0 -18
  46. package/dist/use-waterfall.mjs +0 -41
package/CHANGELOG.md CHANGED
@@ -1,10 +1,48 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.13.0](https://github.com/KATT/raf-query/compare/0.12.0...0.13.0) (2026-11-10)
4
+
5
+ ### ⚠ BREAKING CHANGES
6
+
7
+ * **core:** paging lives in core; useInfiniteQuery reads core's PageControls ([#246](https://github.com/KATT/raf-query/issues/246))
8
+
9
+ ### Features
10
+
11
+ * **core:** paging lives in core; useInfiniteQuery reads core's PageControls ([#246](https://github.com/KATT/raf-query/issues/246)) ([f6b4ed2](https://github.com/KATT/raf-query/commit/f6b4ed210340b4b25bf3316ef08a9c8ceb9ed112))
12
+ * **core:** pass the client making the request to queryFn ([#241](https://github.com/KATT/raf-query/issues/241)) ([8c85746](https://github.com/KATT/raf-query/commit/8c85746c230bc3d8a426b32a130a260fa7e8ca62))
13
+ * **core:** replay cached entries to a new client subscriber ([#243](https://github.com/KATT/raf-query/issues/243)) ([1455eed](https://github.com/KATT/raf-query/commit/1455eed2612fe69f0f10bfec08cfd552ef90c786))
14
+ * **core:** ssr: false for queries only a browser can run ([#240](https://github.com/KATT/raf-query/issues/240)) ([077de62](https://github.com/KATT/raf-query/commit/077de6223a92b8bd8d35f7df043d1efe147ca403))
15
+
16
+ ### Bug Fixes
17
+
18
+ * **core:** infer factories built inline by a generic maker in setData ([#244](https://github.com/KATT/raf-query/issues/244)) ([da87c07](https://github.com/KATT/raf-query/commit/da87c07a3463a919c154ac3ac5fba3d546b3231e))
19
+ * **core:** keep data written while a request ran ([#239](https://github.com/KATT/raf-query/issues/239)) ([4cb7cc0](https://github.com/KATT/raf-query/commit/4cb7cc0605a2d4637591eb365153c5e955eb41a8))
20
+ * **core:** start features once for providers that share a client ([#242](https://github.com/KATT/raf-query/issues/242)) ([d5968fb](https://github.com/KATT/raf-query/commit/d5968fb140429a1b2e7fb94662c266a52e877cc0))
21
+
22
+ ### Documentation
23
+
24
+ * every example is the blog's posts, comments and authors ([#236](https://github.com/KATT/raf-query/issues/236)) ([2551e97](https://github.com/KATT/raf-query/commit/2551e9710aa7b149be333a1675dae31d9d1f5efd)), references [#230](https://github.com/KATT/raf-query/issues/230)
25
+
26
+ ### Miscellaneous Chores
27
+
28
+ * **lint:** require a docs link on every public export ([#237](https://github.com/KATT/raf-query/issues/237)) ([b5c8133](https://github.com/KATT/raf-query/commit/b5c8133c129e0412e82cd2f24da6a2516b4a2012))
29
+ * unblock CI: [@see](https://github.com/see) links and the RSC test's build ([#245](https://github.com/KATT/raf-query/issues/245)) ([84927e4](https://github.com/KATT/raf-query/commit/84927e4a900f7608e1e17b022e28afadbe00e4ae)), references [#243](https://github.com/KATT/raf-query/issues/243) [#246](https://github.com/KATT/raf-query/issues/246) [#240](https://github.com/KATT/raf-query/issues/240) [#238](https://github.com/KATT/raf-query/issues/238)
30
+
31
+ ## [0.12.0](https://github.com/KATT/raf-query/compare/0.11.1...0.12.0) (2026-11-08)
32
+
33
+ ### ⚠ BREAKING CHANGES
34
+
35
+ - greenfield rewrite on a promise kernel, with load() handles and opt-in behaviors ([#230](https://github.com/KATT/raf-query/issues/230))
36
+
37
+ ### Features
38
+
39
+ - 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))
40
+
3
41
  ## [0.11.1](https://github.com/KATT/raf-query/compare/0.11.0...0.11.1) (2026-10-17)
4
42
 
5
43
  ### Features
6
44
 
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)
45
+ - **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
46
 
9
47
  ## [0.11.0](https://github.com/KATT/raf-query/compare/0.10.4...0.11.0) (2026-10-16)
10
48
 
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,21 @@
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
+ * @see https://raf-query.katt.dev/docs/links-and-features#typing-meta-by-its-root
19
+ */
20
+ export declare function bindUseQueryClient<T>(): () => QueryClient<RootOf<T>>;
21
+ //#endregion
package/dist/bind.mjs ADDED
@@ -0,0 +1,23 @@
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
+ * @see https://raf-query.katt.dev/docs/links-and-features#typing-meta-by-its-root
18
+ */
19
+ function bindUseQueryClient() {
20
+ return useQueryClient;
21
+ }
22
+ //#endregion
23
+ 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,42 @@
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
+ /**
5
+ * Props of {@link QueryDevtoolsPanel}.
6
+ *
7
+ * @see https://raf-query.katt.dev/docs/devtools#props
8
+ */
9
+ export interface QueryDevtoolsPanelProps {
10
+ /** The client to inspect. Default: the nearest `QueryClientProvider`'s. */
11
+ readonly client?: QueryClient;
12
+ /** Shows a close button that calls this. */
13
+ readonly onClose?: () => void;
14
+ /** Styles for the panel's root element, e.g. its height. */
15
+ readonly style?: CSSProperties;
16
+ }
17
+ /**
18
+ * Props of {@link QueryDevtools}.
19
+ *
20
+ * @see https://raf-query.katt.dev/docs/devtools
21
+ */
22
+ export interface QueryDevtoolsProps {
23
+ /** Corner of the toggle button. Default: `"bottom-right"`. */
24
+ readonly buttonPosition?: "bottom-left" | "bottom-right" | "top-left" | "top-right";
25
+ /** The client to inspect. Default: the nearest `QueryClientProvider`'s. */
26
+ readonly client?: QueryClient;
27
+ /** Open on the first render. */
28
+ readonly initialIsOpen?: boolean;
29
+ }
30
+ /**
31
+ * The devtools panel, to place in your own layout.
32
+ *
33
+ * @see https://raf-query.katt.dev/docs/devtools#embedding-the-panel
34
+ */
35
+ export declare function QueryDevtoolsPanel(props: QueryDevtoolsPanelProps): import("react").JSX.Element;
36
+ /**
37
+ * A toggle button that opens the devtools panel over the page.
38
+ *
39
+ * @see https://raf-query.katt.dev/docs/devtools
40
+ */
41
+ export declare function QueryDevtools(props: QueryDevtoolsProps): import("react").JSX.Element;
42
+ //#endregion
package/dist/devtools.mjs CHANGED
@@ -1,3 +1,246 @@
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
+ /**
183
+ * The devtools panel, to place in your own layout.
184
+ *
185
+ * @see https://raf-query.katt.dev/docs/devtools#embedding-the-panel
186
+ */
187
+ function QueryDevtoolsPanel(props) {
188
+ const nearest = useOptionalQueryClient();
189
+ const client = props.client ?? nearest;
190
+ if (!client) throw new Error("QueryDevtoolsPanel: pass `client`, or render it in a QueryClientProvider");
191
+ return /* @__PURE__ */ jsx(Panel, {
192
+ ...props,
193
+ client
194
+ });
195
+ }
196
+ const corners = {
197
+ "bottom-left": {
198
+ bottom: 12,
199
+ left: 12
200
+ },
201
+ "bottom-right": {
202
+ bottom: 12,
203
+ right: 12
204
+ },
205
+ "top-left": {
206
+ left: 12,
207
+ top: 12
208
+ },
209
+ "top-right": {
210
+ right: 12,
211
+ top: 12
212
+ }
213
+ };
214
+ /**
215
+ * A toggle button that opens the devtools panel over the page.
216
+ *
217
+ * @see https://raf-query.katt.dev/docs/devtools
218
+ */
219
+ function QueryDevtools(props) {
220
+ const [open, setOpen] = useState(props.initialIsOpen ?? false);
221
+ const corner = corners[props.buttonPosition ?? "bottom-right"];
222
+ return /* @__PURE__ */ jsx(Fragment, { children: open ? /* @__PURE__ */ jsx("div", {
223
+ style: {
224
+ bottom: 0,
225
+ left: 0,
226
+ position: "fixed",
227
+ right: 0,
228
+ zIndex: 99999
229
+ },
230
+ children: /* @__PURE__ */ jsx(QueryDevtoolsPanel, {
231
+ client: props.client,
232
+ onClose: () => setOpen(false)
233
+ })
234
+ }) : /* @__PURE__ */ jsx("button", {
235
+ onClick: () => setOpen(true),
236
+ style: {
237
+ ...corner,
238
+ position: "fixed",
239
+ zIndex: 99999
240
+ },
241
+ type: "button",
242
+ children: "raf-query"
243
+ }) });
244
+ }
245
+ //#endregion
3
246
  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,12 @@ 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.
13
+ *
14
+ * @see https://raf-query.katt.dev/docs/react-server-components#setup
9
15
  */
10
16
  export declare function HydrationBoundary(props: HydrationBoundaryProps): import("react").JSX.Element;
11
17
  //#endregion