@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.
- package/CHANGELOG.md +39 -1
- package/README.md +32 -59
- package/dist/bind.d.mts +21 -0
- package/dist/bind.mjs +23 -0
- package/dist/browser-render.mjs +46 -0
- package/dist/devtools.d.mts +42 -2
- package/dist/devtools.mjs +244 -1
- package/dist/hydration-boundary/hydrate.mjs +10 -13
- package/dist/hydration-boundary/hydration-boundary-react-server.d.mts +6 -0
- package/dist/hydration-boundary/hydration-boundary-react-server.mjs +20 -9
- package/dist/hydration-boundary/hydration-boundary.d.mts +15 -28
- package/dist/hydration-boundary/hydration-boundary.mjs +7 -26
- package/dist/index-react-server.d.mts +7 -6
- package/dist/index-react-server.mjs +4 -6
- package/dist/index-shared.d.mts +5 -6
- package/dist/index-shared.mjs +3 -6
- package/dist/index.d.mts +7 -6
- package/dist/index.mjs +5 -4
- package/dist/provider.d.mts +11 -5
- package/dist/provider.mjs +12 -9
- package/dist/react-server-hooks.d.mts +43 -0
- package/dist/react-server-hooks.mjs +45 -0
- package/dist/tanstack-router-browser.d.mts +2 -0
- package/dist/tanstack-router-browser.mjs +2 -0
- package/dist/tanstack-router-shared.d.mts +9 -7
- package/dist/tanstack-router-shared.mjs +12 -22
- package/dist/tanstack-router.d.mts +4 -2
- package/dist/tanstack-router.mjs +12 -14
- package/dist/tracked.d.mts +1 -0
- package/dist/tracked.mjs +65 -0
- package/dist/use-infinite-query.d.mts +23 -0
- package/dist/use-infinite-query.mjs +30 -0
- package/dist/use-is-fetching.d.mts +7 -15
- package/dist/use-is-fetching.mjs +12 -30
- package/dist/use-query.d.mts +52 -50
- package/dist/use-query.mjs +112 -47
- package/package.json +10 -9
- package/dist/devtools-panel.d.mts +0 -38
- package/dist/devtools-panel.mjs +0 -896
- package/dist/devtools-store.mjs +0 -104
- package/dist/hydration-boundary/sent.mjs +0 -16
- package/dist/query-provider/browser-requests.mjs +0 -38
- package/dist/query-provider/query-provider.d.mts +0 -21
- package/dist/query-provider/query-provider.mjs +0 -33
- package/dist/use-waterfall.d.mts +0 -18
- package/dist/use-waterfall.mjs +0 -41
package/dist/tracked.mjs
ADDED
|
@@ -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 {
|
|
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
|
|
5
|
-
*
|
|
6
|
-
* while hydrating, so the
|
|
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
|
-
*
|
|
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(
|
|
11
|
+
export declare function useIsFetching(target?: AnyFactory | AnyTarget | Group): number;
|
|
20
12
|
//#endregion
|
package/dist/use-is-fetching.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
19
|
-
*
|
|
20
|
-
* while hydrating, so the
|
|
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
|
-
*
|
|
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(
|
|
13
|
+
function useIsFetching(target) {
|
|
34
14
|
const client = useQueryClient();
|
|
35
|
-
const subscribe = useCallback((
|
|
36
|
-
|
|
37
|
-
|
|
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 };
|
package/dist/use-query.d.mts
CHANGED
|
@@ -1,62 +1,64 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
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
|
-
*
|
|
9
|
-
* `use()` and the query's controls:
|
|
6
|
+
* Options for `useQuery` and `useInfiniteQuery`.
|
|
10
7
|
*
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
*
|
|
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
|
-
*
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
*
|
|
41
|
-
|
|
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 [
|
|
53
|
+
* const [postThenable, postState] = useQuery(postQuery, handles.post);
|
|
54
|
+
* const post = use(postThenable);
|
|
45
55
|
* ```
|
|
46
56
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
package/dist/use-query.mjs
CHANGED
|
@@ -1,56 +1,121 @@
|
|
|
1
|
-
"use client";
|
|
2
1
|
import { useQueryClient } from "./provider.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const controls =
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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.
|
|
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.
|
|
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
|
-
"@
|
|
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
|
-
"
|
|
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
|