@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
@@ -0,0 +1,65 @@
1
+ //#region src/tracked.ts
2
+ function createTracker() {
3
+ return {
4
+ actions: /* @__PURE__ */ new Map(),
5
+ proxy: void 0,
6
+ rendered: void 0,
7
+ tracked: /* @__PURE__ */ new Set()
8
+ };
9
+ }
10
+ function renderedOf(tracker) {
11
+ return tracker.rendered;
12
+ }
13
+ /** A function that calls the latest render's, so it keeps its identity across renders. */
14
+ function stableAction(tracker, name) {
15
+ let action = tracker.actions.get(name);
16
+ if (!action) {
17
+ action = (...args) => {
18
+ const latest = renderedOf(tracker).actions[name];
19
+ return latest(...args);
20
+ };
21
+ tracker.actions.set(name, action);
22
+ }
23
+ return action;
24
+ }
25
+ function proxyOf(tracker, read) {
26
+ return new Proxy({
27
+ ...read.fields,
28
+ ...read.actions
29
+ }, { get(_, key) {
30
+ const name = String(key);
31
+ if (read.actions[name]) return stableAction(tracker, name);
32
+ tracker.tracked.add(name);
33
+ return renderedOf(tracker).fields[name];
34
+ } });
35
+ }
36
+ /** Functions are stable, so only data fields count as changes. */
37
+ function trackedChanged(tracker, next) {
38
+ const previous = renderedOf(tracker).fields;
39
+ return [...tracker.tracked].some((name) => !Object.is(previous[name], next.fields[name]));
40
+ }
41
+ /**
42
+ * The state a render returns: the same object as the last render's unless
43
+ * a field the component read changed. Reads go through to this render's
44
+ * fields, so a field first read now is current, and is tracked from now on.
45
+ */
46
+ function trackedState(tracker, read, refresh) {
47
+ if (tracker.proxy === void 0 || trackedChanged(tracker, read)) tracker.proxy = proxyOf(tracker, read);
48
+ tracker.rendered = {
49
+ ...read,
50
+ refresh,
51
+ status: read.source.status
52
+ };
53
+ return tracker.proxy;
54
+ }
55
+ /**
56
+ * Whether a write changed what the component rendered: the thenable or its
57
+ * outcome, or a field it read. Fields it never read don't re-render it.
58
+ */
59
+ function renderedChanged(tracker) {
60
+ const rendered = renderedOf(tracker);
61
+ const next = rendered.refresh();
62
+ return next.source !== rendered.source || next.source.status !== rendered.status || trackedChanged(tracker, next);
63
+ }
64
+ //#endregion
65
+ export { createTracker, renderedChanged, trackedState };
@@ -0,0 +1,23 @@
1
+ import { QueryState, QueryValue, UseQueryOptions } from "./use-query.mjs";
2
+ import { InfiniteQueryFactory, PageControls, QueryHandle } from "@raf-query/core";
3
+ //#region src/use-infinite-query.d.ts
4
+ /**
5
+ * What `useInfiniteQuery` returns next to the data: core's paging controls
6
+ * and the query's state.
7
+ *
8
+ * @see https://raf-query.katt.dev/docs/infinite-queries#reading-pages
9
+ */
10
+ export interface InfiniteQueryState<TFactory extends InfiniteQueryFactory = InfiniteQueryFactory> extends PageControls, QueryState<TFactory> {}
11
+ /**
12
+ * Read a paged query (`infiniteQueryOptions`) by its handle. The data is
13
+ * `{ pages, pageParams }`, and its state loads more pages.
14
+ *
15
+ * ```tsx
16
+ * const [pagesThenable, pagesState] = useInfiniteQuery(commentsQuery, handles.comments);
17
+ * const pages = use(pagesThenable);
18
+ * ```
19
+ *
20
+ * @see https://raf-query.katt.dev/docs/infinite-queries#reading-pages
21
+ */
22
+ export declare function useInfiniteQuery<TFactory extends InfiniteQueryFactory>(factory: TFactory, handle: QueryHandle<TFactory>, options?: UseQueryOptions): readonly [QueryValue<TFactory>, InfiniteQueryState<TFactory>];
23
+ //#endregion
@@ -0,0 +1,30 @@
1
+ import { useQueryRead } from "./use-query.mjs";
2
+ import { fetchPage, pageState } from "@raf-query/core/internal";
3
+ //#region src/use-infinite-query.ts
4
+ /** Core's paging controls, split into stable actions and tracked fields. */
5
+ function pageFields(entry, state) {
6
+ return {
7
+ actions: {
8
+ fetchNextPage: () => fetchPage(entry, "next"),
9
+ fetchPreviousPage: () => fetchPage(entry, "previous")
10
+ },
11
+ fields: { ...pageState(entry, state) }
12
+ };
13
+ }
14
+ /**
15
+ * Read a paged query (`infiniteQueryOptions`) by its handle. The data is
16
+ * `{ pages, pageParams }`, and its state loads more pages.
17
+ *
18
+ * ```tsx
19
+ * const [pagesThenable, pagesState] = useInfiniteQuery(commentsQuery, handles.comments);
20
+ * const pages = use(pagesThenable);
21
+ * ```
22
+ *
23
+ * @see https://raf-query.katt.dev/docs/infinite-queries#reading-pages
24
+ */
25
+ function useInfiniteQuery(factory, handle, options) {
26
+ const read = useQueryRead(factory, handle, options, pageFields);
27
+ return [read.thenable, read.controls];
28
+ }
29
+ //#endregion
30
+ export { useInfiniteQuery };
@@ -1,20 +1,12 @@
1
- import { QueryCache } from "@raf-query/core";
1
+ import { Group } from "@raf-query/core";
2
+ import { AnyFactory, AnyTarget } from "@raf-query/core/internal";
2
3
  //#region src/use-is-fetching.d.ts
3
4
  /**
4
- * How many queries have a request in flight: all of them, or those matching
5
- * `filter`. Re-renders when the count changes. It's `0` on the server and
6
- * while hydrating, so the server's HTML and the browser's first render agree.
5
+ * How many queries have a request in flight: all of them, or a factory's,
6
+ * a group's or a target's. For a global loading indicator. Always `0` on
7
+ * the server and while hydrating, so the HTML matches the first browser render.
7
8
  *
8
- * You probably don't need this. Prefer the router's pending UI for
9
- * navigations (`useRouterState` / `pendingComponent` in TanStack Router,
10
- * `useLinkStatus` in Next.js) and per-query `controls.isFetching` for
11
- * background refetches. Kept for TanStack Query compatibility when you want
12
- * one global count of every in-flight request:
13
- *
14
- * ```tsx
15
- * const fetching = useIsFetching();
16
- * return fetching > 0 ? <ProgressBar /> : null;
17
- * ```
9
+ * @see https://raf-query.katt.dev/docs/background-fetching-indicators#useisfetching
18
10
  */
19
- export declare function useIsFetching(filter?: QueryCache.Filter): number;
11
+ export declare function useIsFetching(target?: AnyFactory | AnyTarget | Group): number;
20
12
  //#endregion
@@ -1,40 +1,22 @@
1
- "use client";
2
1
  import { useQueryClient } from "./provider.mjs";
2
+ import { isFetching } from "@raf-query/core";
3
3
  import { useCallback, useSyncExternalStore } from "react";
4
4
  //#region src/use-is-fetching.ts
5
- function subscribeDeferred(client, onStoreChange) {
6
- let scheduled = false;
7
- return client.subscribe(() => {
8
- if (scheduled) return;
9
- scheduled = true;
10
- queueMicrotask(() => {
11
- scheduled = false;
12
- onStoreChange();
13
- });
14
- });
15
- }
16
- const none = () => 0;
5
+ const serverCount = () => 0;
17
6
  /**
18
- * How many queries have a request in flight: all of them, or those matching
19
- * `filter`. Re-renders when the count changes. It's `0` on the server and
20
- * while hydrating, so the server's HTML and the browser's first render agree.
21
- *
22
- * You probably don't need this. Prefer the router's pending UI for
23
- * navigations (`useRouterState` / `pendingComponent` in TanStack Router,
24
- * `useLinkStatus` in Next.js) and per-query `controls.isFetching` for
25
- * background refetches. Kept for TanStack Query compatibility when you want
26
- * one global count of every in-flight request:
7
+ * How many queries have a request in flight: all of them, or a factory's,
8
+ * a group's or a target's. For a global loading indicator. Always `0` on
9
+ * the server and while hydrating, so the HTML matches the first browser render.
27
10
  *
28
- * ```tsx
29
- * const fetching = useIsFetching();
30
- * return fetching > 0 ? <ProgressBar /> : null;
31
- * ```
11
+ * @see https://raf-query.katt.dev/docs/background-fetching-indicators#useisfetching
32
12
  */
33
- function useIsFetching(filter) {
13
+ function useIsFetching(target) {
34
14
  const client = useQueryClient();
35
- const subscribe = useCallback((onStoreChange) => subscribeDeferred(client, onStoreChange), [client]);
36
- const getSnapshot = () => client.isFetching(filter);
37
- return useSyncExternalStore(subscribe, getSnapshot, none);
15
+ const subscribe = useCallback((onChange) => client.subscribe((event) => {
16
+ if (event.type === "updated" || event.type === "removed") onChange();
17
+ }), [client]);
18
+ const count = () => isFetching(client, target);
19
+ return useSyncExternalStore(subscribe, count, serverCount);
38
20
  }
39
21
  //#endregion
40
22
  export { useIsFetching };
@@ -1,62 +1,64 @@
1
- import { DataOf, ErrorOf, QueryFactory, QueryHandle, QueryHandleWithData, Thenable, ThrowsOnError, UseQueryControls, UseQueryOptions, UseQueryThrowOptions } from "@raf-query/core";
2
- import { Result } from "effect";
1
+ import "./tracked.mjs";
2
+ import { DataOf, InputOf, QueryFactory, QueryHandle, SingleQueryFactory, Thenable } from "@raf-query/core";
3
+ import "@raf-query/core/internal";
3
4
  //#region src/use-query.d.ts
4
- export type UseQueryResult<TFactory extends QueryFactory, TThrow extends boolean | undefined = undefined> = ThrowsOnError<TFactory, TThrow> extends true ? readonly [promise: Thenable<DataOf<TFactory>>, controls: UseQueryControls<TFactory>] : readonly [promise: Thenable<Result.Result<DataOf<TFactory>, ErrorOf<TFactory>>>, controls: UseQueryControls<TFactory>];
5
- /** {@link useQuery}'s result for a handle from `ensure` or `fetch`. */
6
- export type UseQueryResultWithData<TFactory extends QueryFactory, TThrow extends boolean | undefined = undefined> = ThrowsOnError<TFactory, TThrow> extends true ? readonly [data: DataOf<TFactory>, controls: UseQueryControls<TFactory>] : readonly [result: Result.Result<DataOf<TFactory>, ErrorOf<TFactory>>, controls: UseQueryControls<TFactory>];
7
5
  /**
8
- * Read the query a handle points at. Returns a promise to unwrap with
9
- * `use()` and the query's controls:
6
+ * Options for `useQuery` and `useInfiniteQuery`.
10
7
  *
11
- * ```tsx
12
- * const [postPromise, postState] = useQuery(postQuery, loaderData.post);
13
- * const post = use(postPromise);
14
- * ```
15
- *
16
- * For an Effect `queryFn` (typed {@link ErrorOf}), with `throwOnError: false`
17
- * (the call's, else the query's, else the client's, which is the default
18
- * when it has a `layer`), the first slot is a `Result` instead — `use()`
19
- * never throws:
20
- *
21
- * ```tsx
22
- * const [issueResult, issueState] = useQuery(issueQuery, loaderData.issue);
23
- * const issue = use(issueResult);
24
- * Result.match(issue, { onFailure: …, onSuccess: … });
25
- * ```
26
- *
27
- * The promise is stable per data version and already fulfilled once the
28
- * cache has data, so `use()` only suspends while there is nothing to show.
29
- * The component re-renders when the promise changes, and when a control it
30
- * has read changes; changes to controls it never reads are skipped. A
31
- * first load settles its pending promise in place: that counts as a change
32
- * once the component has read the promise's `status`, `value` or `reason`,
33
- * so reading them without `use()` re-renders when the data lands:
8
+ * @see https://raf-query.katt.dev/docs/url-state-and-pagination#what-shows-while-the-new-input-loads
9
+ */
10
+ export interface UseQueryOptions {
11
+ /**
12
+ * While a new handle's query has no data: `"suspend"` (the default) shows
13
+ * the Suspense fallback; `"previous"` keeps showing the last handle's data
14
+ * and sets `isPrevious`.
15
+ */
16
+ readonly whilePending?: "previous" | "suspend";
17
+ }
18
+ /**
19
+ * What `useQuery` returns next to the data.
34
20
  *
35
- * ```tsx
36
- * const [tagPromise] = useQuery(tagQuery, loaderData.tag);
37
- * const tag = tagPromise.status === "fulfilled" ? tagPromise.value : "…";
38
- * ```
21
+ * @see https://raf-query.katt.dev/docs/reading-queries#state
22
+ */
23
+ export interface QueryState<TFactory extends QueryFactory = QueryFactory> {
24
+ /** The last request's error while it's the latest outcome. */
25
+ readonly error: unknown;
26
+ readonly hash: string;
27
+ readonly input: InputOf<TFactory>;
28
+ readonly isFetching: boolean;
29
+ /** No data yet. */
30
+ readonly isPending: boolean;
31
+ /** Showing the previous handle's data (`whilePending: "previous"`). */
32
+ readonly isPrevious: boolean;
33
+ readonly isStale: boolean;
34
+ readonly queryKey: ReturnType<TFactory>["queryKey"];
35
+ /** Fetch again, or join the request in flight. Resolves when it settles. */
36
+ refetch(): Promise<void>;
37
+ /** When the data was fetched (ms since epoch); `0` without data. */
38
+ readonly updatedAt: number;
39
+ }
40
+ /**
41
+ * What `useQuery` and `useInfiniteQuery` read, for every handle: a thenable
42
+ * to pass to `use()`.
43
+ * A loader can't promise the data is still cached when a component renders
44
+ * (a write can reset or remove it), so the read may suspend.
39
45
  *
40
- * A handle from `ensure` or `fetch` already has its data, so the hook
41
- * unwraps the promise itself and returns the data (or `Result`):
46
+ * @see https://raf-query.katt.dev/docs/reading-queries
47
+ */
48
+ export type QueryValue<TFactory extends QueryFactory> = Thenable<DataOf<TFactory>>;
49
+ /**
50
+ * Read a query by its handle.
42
51
  *
43
52
  * ```tsx
44
- * const [post, postState] = useQuery(postQuery, loaderData.post);
53
+ * const [postThenable, postState] = useQuery(postQuery, handles.post);
54
+ * const post = use(postThenable);
45
55
  * ```
46
56
  *
47
- * It reads the live entry, not the handle's `data` snapshot. If that entry
48
- * has lost its data since (garbage-collected, `reset`), the hook suspends
49
- * until it is refetched.
57
+ * Reading never refetches stale data: loaders decide freshness. A query no
58
+ * loader started fetches while rendering, which is a waterfall (see
59
+ * `waterfallHandle`). Paged queries read with `useInfiniteQuery`.
50
60
  *
51
- * The hook picks by the handle at runtime (`data` in it), so a prop that
52
- * receives a handle from `ensure` / `fetch` must be typed
53
- * {@link QueryHandleWithData}: typed as `QueryHandle`, it reads as a promise
54
- * while the hook returns the data.
61
+ * @see https://raf-query.katt.dev/docs/reading-queries
55
62
  */
56
- export declare function useQuery<TFactory extends QueryFactory, const TThrow extends boolean | undefined = undefined>(factory: TFactory, handle: QueryHandleWithData<TFactory>, options?: UseQueryOptions & UseQueryThrowOptions<TFactory> & {
57
- readonly throwOnError?: TThrow;
58
- }): UseQueryResultWithData<TFactory, TThrow>;
59
- export declare function useQuery<TFactory extends QueryFactory, const TThrow extends boolean | undefined = undefined>(factory: TFactory, handle: QueryHandle<TFactory>, options?: UseQueryOptions & UseQueryThrowOptions<TFactory> & {
60
- readonly throwOnError?: TThrow;
61
- }): UseQueryResult<TFactory, TThrow>;
63
+ export declare function useQuery<TFactory extends SingleQueryFactory>(factory: TFactory, handle: QueryHandle<TFactory>, options?: UseQueryOptions): readonly [QueryValue<TFactory>, QueryState<TFactory>];
62
64
  //#endregion
@@ -1,56 +1,121 @@
1
- "use client";
2
1
  import { useQueryClient } from "./provider.mjs";
3
- import { assertSent } from "./query-provider/browser-requests.mjs";
4
- import { Query, QueryBinding, QueryObserver, promiseControls, resultThenable, throwsOnError } from "@raf-query/core";
5
- import { use, useEffect, useState, useSyncExternalStore } from "react";
2
+ import { browserRender, leavesUnstartedToBrowser } from "./browser-render.mjs";
3
+ import { createTracker, renderedChanged, trackedState } from "./tracked.mjs";
4
+ import { entryOf, fetchEntry, isBrowserOnly, isInvalidated, isStale, isWaterfallHandle, mapThenable, subscribeEntry } from "@raf-query/core/internal";
5
+ import { useCallback, useState, useSyncExternalStore } from "react";
6
6
  //#region src/use-query.ts
7
- function storeFor(query, options) {
8
- const observer = QueryObserver.makeUnsafe(query, options);
7
+ /** Whether nothing is loading the entry: no data, no error, no request in flight. */
8
+ function isIdle(entry, state) {
9
+ return !state.hasData && !entry.run && state.status !== "error";
10
+ }
11
+ /**
12
+ * Start the request of an entry nothing is loading: the reader of a
13
+ * waterfall handle, or of a query the cache dropped. A server render fed by
14
+ * server components leaves it to the browser: nothing would send its data.
15
+ */
16
+ /**
17
+ * Whether a server render leaves this idle entry to the browser: on a client
18
+ * fed by server components, or a query only the browser runs (`ssr: false`).
19
+ */
20
+ function leftToBrowser(entry) {
21
+ return leavesUnstartedToBrowser(entry.client) || isBrowserOnly(entry);
22
+ }
23
+ function startIfMissing(entry, state, waterfall) {
24
+ const client = entry.client;
25
+ if (!isIdle(entry, state) || leftToBrowser(entry)) return;
26
+ const expected = waterfall || entry.options?.ssr === false;
27
+ if (process.env.NODE_ENV !== "production" && !expected && !client.isServer) console.warn(`raf-query: ${entry.hash} was read before anything loaded it, so it fetches while rendering. Start it in a loader with load(), or build the handle with waterfallHandle() to make the waterfall explicit.`);
28
+ fetchEntry(entry);
29
+ }
30
+ /**
31
+ * In a server render, an entry `startIfMissing` left to the browser reads as
32
+ * a rejection: a `use()` of it inside Suspense renders that boundary's
33
+ * fallback, and the browser renders the content.
34
+ */
35
+ function readOf(entry, state) {
36
+ return leftToBrowser(entry) && isIdle(entry, state) ? browserRender(entry) : entry.thenable;
37
+ }
38
+ function settled(promise) {
39
+ return promise.then(() => {});
40
+ }
41
+ /**
42
+ * One read of the entry. It takes both entries' states as arguments: core
43
+ * writes a new state object with every write (and the thenable that
44
+ * carries it), so a compiler that memoizes the call recomputes it.
45
+ */
46
+ function readQuery(current, state, shown, shownState, handle, options, extra) {
47
+ const isPrevious = options?.whilePending === "previous" && !state.hasData && shown !== current && shownState.hasData;
48
+ const entry = isPrevious ? shown : current;
49
+ const entryState = isPrevious ? shownState : state;
50
+ const added = extra?.(entry, entryState);
9
51
  return {
10
- getSnapshot: () => QueryObserver.getUnsafe(observer),
11
- observer,
12
- subscribe: (onChange) => QueryObserver.subscribeUnsafe(observer, onChange)
52
+ actions: {
53
+ refetch: () => settled(fetchEntry(current)),
54
+ ...added?.actions
55
+ },
56
+ entry,
57
+ fields: {
58
+ error: state.error,
59
+ hash: current.hash,
60
+ input: handle.input,
61
+ isFetching: state.isFetching,
62
+ isPending: !state.hasData,
63
+ isPrevious,
64
+ isStale: isStale(current),
65
+ queryKey: handle.queryKey,
66
+ updatedAt: state.updatedAt,
67
+ ...added?.fields
68
+ },
69
+ source: readOf(entry, entryState)
13
70
  };
14
71
  }
15
- function useQuery(factory, handle, options = {}) {
72
+ /**
73
+ * The read both hooks share: the thenable, and the state with the fields
74
+ * `extra` adds. The state is tracked: the component re-renders, and gets a
75
+ * new state object, only when the thenable or a field it read changes.
76
+ */
77
+ function useQueryRead(factory, handle, options, extra) {
16
78
  const client = useQueryClient();
17
- const query = QueryBinding.queryFor(client, factory, handle);
18
- assertSent(client, query);
19
- Query.readUnsafe(query);
20
- const refetchInterval = options.refetchInterval;
21
- const refetchOnReconnect = options.refetchOnReconnect;
22
- const refetchOnWindowFocus = options.refetchOnWindowFocus;
23
- const whilePending = options.whilePending;
24
- const [store] = useState(() => storeFor(query, options));
25
- const observer = store.observer;
26
- const result = QueryObserver.getOptimisticResultUnsafe(observer, query, options);
27
- useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
28
- useEffect(() => {
29
- QueryObserver.setOptionsUnsafe(observer, query, {
30
- refetchInterval,
31
- refetchOnReconnect,
32
- refetchOnWindowFocus,
33
- whilePending
79
+ const current = entryOf(client, factory, handle.input);
80
+ const [tracker] = useState(createTracker);
81
+ const subscribe = useCallback((onChange) => {
82
+ const unsubscribe = subscribeEntry(current, () => {
83
+ if (renderedChanged(tracker)) onChange();
34
84
  });
35
- }, [
36
- observer,
37
- query,
38
- refetchInterval,
39
- refetchOnReconnect,
40
- refetchOnWindowFocus,
41
- whilePending
42
- ]);
43
- QueryObserver.trackProp(observer, "promise");
44
- const controls = promiseControls(QueryObserver.trackResult(observer, result));
45
- const throwOnError = throwsOnError(query, options.throwOnError, client.throwOnError);
46
- const withData = "data" in handle;
47
- if (!throwOnError) {
48
- const resultPromise = resultThenable(result.promise);
49
- if (withData) return [use(resultPromise), controls];
50
- return [QueryObserver.trackThenable(observer, resultPromise), controls];
51
- }
52
- if (withData) return [use(result.promise), controls];
53
- return [QueryObserver.trackThenable(observer, result.promise), controls];
85
+ if (isInvalidated(current)) fetchEntry(current);
86
+ return unsubscribe;
87
+ }, [current, tracker]);
88
+ const getState = () => current.state;
89
+ const state = useSyncExternalStore(subscribe, getState, getState);
90
+ const [shown, setShown] = useState(current);
91
+ if (shown !== current && state.hasData) setShown(current);
92
+ startIfMissing(current, state, isWaterfallHandle(handle));
93
+ const read = readQuery(current, state, shown, shown.state, handle, options, extra);
94
+ const controls = trackedState(tracker, read, () => readQuery(current, current.state, shown, shown.state, handle, options, extra));
95
+ const selected = useState(() => /* @__PURE__ */ new WeakMap())[0];
96
+ const select = read.entry.options?.select;
97
+ return {
98
+ controls,
99
+ thenable: select ? mapThenable(selected, read.source, select) : read.source
100
+ };
101
+ }
102
+ /**
103
+ * Read a query by its handle.
104
+ *
105
+ * ```tsx
106
+ * const [postThenable, postState] = useQuery(postQuery, handles.post);
107
+ * const post = use(postThenable);
108
+ * ```
109
+ *
110
+ * Reading never refetches stale data: loaders decide freshness. A query no
111
+ * loader started fetches while rendering, which is a waterfall (see
112
+ * `waterfallHandle`). Paged queries read with `useInfiniteQuery`.
113
+ *
114
+ * @see https://raf-query.katt.dev/docs/reading-queries
115
+ */
116
+ function useQuery(factory, handle, options) {
117
+ const read = useQueryRead(factory, handle, options);
118
+ return [read.thenable, read.controls];
54
119
  }
55
120
  //#endregion
56
- export { useQuery };
121
+ export { useQuery, useQueryRead };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raf-query/react",
3
- "version": "0.11.1",
3
+ "version": "0.13.0",
4
4
  "description": "React bindings for raf-query, a render-as-you-fetch query library",
5
5
  "keywords": [
6
6
  "query",
@@ -36,7 +36,6 @@
36
36
  "react-server": "./dist/index-react-server.mjs",
37
37
  "default": "./dist/index.mjs"
38
38
  },
39
- "./devtools": "./dist/devtools.mjs",
40
39
  "./tanstack-router": {
41
40
  "workerd": "./dist/tanstack-router.mjs",
42
41
  "worker": "./dist/tanstack-router.mjs",
@@ -48,11 +47,10 @@
48
47
  }
49
48
  },
50
49
  "dependencies": {
51
- "@raf-query/core": "0.11.1"
50
+ "@raf-query/core": "0.13.0"
52
51
  },
53
52
  "peerDependencies": {
54
53
  "@tanstack/react-router": "^1.170.0",
55
- "effect": "^4.0.0",
56
54
  "react": "^19.2.0"
57
55
  },
58
56
  "peerDependenciesMeta": {
@@ -61,19 +59,22 @@
61
59
  }
62
60
  },
63
61
  "devDependencies": {
64
- "@effect/vitest": "4.0.1",
62
+ "@babel/core": "7.29.7",
63
+ "@rolldown/plugin-babel": "0.2.4",
65
64
  "@tanstack/react-router": "1.170.42",
66
65
  "@testing-library/react": "16.3.3",
67
66
  "@types/node": "26.6.4",
68
67
  "@types/react": "19.3.0",
69
68
  "@types/react-dom": "19.3.0",
69
+ "@vitejs/plugin-react": "6.1.2",
70
70
  "@vitest/coverage-v8": "5.0.1",
71
- "effect": "^4.0.1",
71
+ "babel-plugin-react-compiler": "1.0.0",
72
72
  "happy-dom": "21.0.1",
73
73
  "react": "19.3.0",
74
74
  "react-dom": "19.3.0",
75
75
  "typescript": "7.0.2",
76
- "vite-plus": "1.0.0"
76
+ "vite-plus": "1.0.0",
77
+ "vitest": "5.0.1"
77
78
  },
78
79
  "publishConfig": {
79
80
  "access": "public",
@@ -84,8 +85,8 @@
84
85
  },
85
86
  "scripts": {
86
87
  "build": "vp pack",
87
- "test": "vp test",
88
- "typecheck": "tsc",
88
+ "test": "vp test --config vitest.compiler.config.ts && vp test",
89
+ "typecheck": "tsc && tsc -p tsconfig.no-node.json",
89
90
  "publish:npm": "pnpm pack --out package.tgz && npm publish package.tgz --access public && rm package.tgz"
90
91
  }
91
92
  }
@@ -1,38 +0,0 @@
1
- import { QueryClient } from "@raf-query/core";
2
- import { CSSProperties } from "react";
3
- //#region src/devtools-panel.d.ts
4
- export type DevtoolsTheme = "dark" | "light" | "system";
5
- export interface QueryDevtoolsPanelProps {
6
- /** The client to inspect. Default: the nearest `QueryClientProvider`'s. */
7
- readonly client?: QueryClient;
8
- /** Shows a close button that calls this. */
9
- readonly onClose?: () => void;
10
- /** Styles for the panel's root element, e.g. its height. */
11
- readonly style?: CSSProperties;
12
- /** Default: `"system"`. */
13
- readonly theme?: DevtoolsTheme;
14
- }
15
- export interface QueryDevtoolsProps {
16
- /** Corner of the toggle button. Default: `"bottom-right"`. */
17
- readonly buttonPosition?: "bottom-left" | "bottom-right" | "top-left" | "top-right";
18
- /** The client to inspect. Default: the nearest `QueryClientProvider`'s. */
19
- readonly client?: QueryClient;
20
- /** Open on the first visit. Later visits remember the last open state. */
21
- readonly initialIsOpen?: boolean;
22
- /** Edge of the viewport the panel docks to. Default: `"bottom"`. */
23
- readonly position?: "bottom" | "left" | "right" | "top";
24
- /** Default: `"system"`. */
25
- readonly theme?: DevtoolsTheme;
26
- }
27
- /**
28
- * The devtools panel on its own, to place anywhere in the page. Renders
29
- * nothing on the server, since the server's cache isn't the browser's.
30
- */
31
- export declare function QueryDevtoolsPanel(props: QueryDevtoolsPanelProps): import("react").JSX.Element | null;
32
- /**
33
- * A toggle button that opens the devtools panel docked to an edge of the
34
- * viewport. Renders nothing on the server. Open state and size are kept in
35
- * `localStorage`.
36
- */
37
- export declare function QueryDevtools(props: QueryDevtoolsProps): import("react").JSX.Element | null;
38
- //#endregion