@raf-query/react 0.0.13 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,10 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.0.14](https://github.com/KATT/raf-query/compare/0.0.13...0.0.14) (2026-10-12)
4
+
5
+ ### Features
6
+
7
+ * tracked query results via a core QueryObserver, like TanStack Query ([#13](https://github.com/KATT/raf-query/issues/13)) ([7b99b47](https://github.com/KATT/raf-query/commit/7b99b4704be110c999b41277afd5dac4c515f45f))
8
+
3
9
  ## [0.0.13](https://github.com/KATT/raf-query/compare/0.0.12...0.0.13) (2026-10-12)
4
10
 
5
11
  ### Features
6
12
 
7
- * useQuery returns data for handles from ensure ([#16](https://github.com/KATT/raf-query/issues/16)) ([a50b8fd](https://github.com/KATT/raf-query/commit/a50b8fdc51b952341377d4b55f25ebb6b21d39e1))
13
+ - useQuery returns data for handles from ensure ([#16](https://github.com/KATT/raf-query/issues/16)) ([a50b8fd](https://github.com/KATT/raf-query/commit/a50b8fdc51b952341377d4b55f25ebb6b21d39e1))
8
14
 
9
15
  ## [0.0.12](https://github.com/KATT/raf-query/compare/0.0.11...0.0.12) (2026-10-12)
10
16
 
package/README.md CHANGED
@@ -189,7 +189,7 @@ function PostPage() {
189
189
  | `hash` | Stable string form of the handle's key, for `key` props on Suspense boundaries |
190
190
  | `refetch()` | Refetch now. Returns a promise, so it can be awaited inside a transition or action |
191
191
 
192
- When the cache updates — refetch, invalidation, `setData` — the hook returns a new, pre-resolved promise and the component re-renders without suspending. New data is structurally shared with the old: objects and arrays that are deeply equal keep their identity, and a refetch that returns equal data keeps the same promise, so nothing re-renders. When the **first** load fails, the promise rejects and `use()` throws to the nearest error boundary. Resetting that boundary refetches the query.
192
+ When the cache updates — refetch, invalidation, `setData` — the hook returns a new, pre-resolved promise and the component re-renders without suspending. New data is structurally shared with the old: objects and arrays that are deeply equal keep their identity, and a refetch that returns equal data keeps the same promise, so nothing re-renders. Controls are tracked: reading `controls.isFetching` subscribes the component to `isFetching`, and a change to a control it never read doesn't re-render it. When the **first** load fails, the promise rejects and `use()` throws to the nearest error boundary. Resetting that boundary refetches the query.
193
193
 
194
194
  Queries do not refetch on mount. They refetch when a loader starts them again, when invalidated, on window focus or reconnect while stale, on `refetch()`, or on `refetchInterval`.
195
195
 
@@ -304,7 +304,7 @@ return (
304
304
  );
305
305
  ```
306
306
 
307
- The parent re-renders when `controls` change. If it only passes the promise down, call the hook in the child instead.
307
+ A component re-renders when the promise changes, and when a control it reads changes. Controls it never reads, like `isFetching` here, don't re-render it, so a parent that only passes the promise down stays put during background refetches.
308
308
 
309
309
  ## Render-time inputs: `useWaterfall`
310
310
 
@@ -557,18 +557,19 @@ Query options, accepted by `queryOptions`, `infiniteQueryOptions` and `config.de
557
557
 
558
558
  `@raf-query/core`:
559
559
 
560
- | Export | Use |
561
- | ---------------------------------------------- | ---------------------------------------------------------- |
562
- | `QueryClient` | The cache |
563
- | `queryOptions(options)` | Typed options for a query factory |
564
- | `infiniteQueryOptions(options)` | Typed options for a paged query factory |
565
- | `allKeyed(record)` | Await a record of promises, keeping keys |
566
- | `dehydrate(client)` / `hydrate(client, state)` | Low-level cache transfer used by the integrations |
567
- | `hashKey(key)`, `matchesKey(key, prefix)` | Key helpers |
568
- | `CancelledError` | Rejection of a request cancelled by `clear()` or a restart |
569
- | `QueryHandle<TFactory>` | Handle type for props |
570
- | `EnsuredQueryHandle<TFactory>` | Handle type for props fed by `ensure` |
571
- | `DataOf<TFactory>`, `InputOf<TFactory>` | A factory's data and input types |
560
+ | Export | Use |
561
+ | ---------------------------------------------- | ---------------------------------------------------------------- |
562
+ | `QueryClient` | The cache |
563
+ | `queryOptions(options)` | Typed options for a query factory |
564
+ | `infiniteQueryOptions(options)` | Typed options for a paged query factory |
565
+ | `allKeyed(record)` | Await a record of promises, keeping keys |
566
+ | `dehydrate(client)` / `hydrate(client, state)` | Low-level cache transfer used by the integrations |
567
+ | `hashKey(key)`, `matchesKey(key, prefix)` | Key helpers |
568
+ | `QueryObserver` | One reader's view of a query, with tracked results; for bindings |
569
+ | `CancelledError` | Rejection of a request cancelled by `clear()` or a restart |
570
+ | `QueryHandle<TFactory>` | Handle type for props |
571
+ | `EnsuredQueryHandle<TFactory>` | Handle type for props fed by `ensure` |
572
+ | `DataOf<TFactory>`, `InputOf<TFactory>` | A factory's data and input types |
572
573
 
573
574
  `@raf-query/react` (plus all of the above):
574
575
 
@@ -1,19 +1,7 @@
1
- import { DataOf, EnsuredQueryHandle, InputOf, IsPaged, QueryClient, QueryFactory, QueryHandle, QueryKey } from "@raf-query/core";
1
+ import { DataOf, EnsuredQueryHandle, InputOf, IsPaged, QueryClient, QueryFactory, QueryHandle, QueryKey, QueryObserverOptions } from "@raf-query/core";
2
2
  //#region src/use-query.d.ts
3
- export interface UseQueryOptions {
4
- /** Refetch on this interval (ms) while mounted and the page is visible. */
5
- readonly refetchInterval?: number;
6
- /** Refetch on reconnect while stale. Default: the query's option, else `true`. */
7
- readonly refetchOnReconnect?: boolean;
8
- /** Refetch on window focus while stale. Default: the query's option, else `true`. */
9
- readonly refetchOnWindowFocus?: boolean;
10
- /**
11
- * What to render while the handle's input has no data yet:
12
- * - `"suspend"` (default): the promise is pending, so `use()` suspends.
13
- * - `"previous"`: keep returning the last input's data, with `isPrevious`.
14
- */
15
- readonly whilePending?: "previous" | "suspend";
16
- }
3
+ /** `refetchInterval`, `refetchOnWindowFocus`, `refetchOnReconnect`, `whilePending`. */
4
+ export type UseQueryOptions = QueryObserverOptions;
17
5
  /** The second element of {@link useQuery}'s result. */
18
6
  export interface QueryControls<TFactory extends QueryFactory> {
19
7
  /** Error of the last failed request. Existing data stays readable. */
@@ -61,6 +49,8 @@ export type UseEnsuredQueryResult<TFactory extends QueryFactory> = readonly [dat
61
49
  *
62
50
  * The promise is stable per data version and already fulfilled once the
63
51
  * cache has data, so `use()` only suspends while there is nothing to show.
52
+ * The component re-renders when the promise changes, and when a control it
53
+ * has read changes; changes to controls it never reads are skipped.
64
54
  *
65
55
  * A handle from `ensure` had its data loaded before the route rendered, so
66
56
  * the hook returns the data itself:
@@ -1,71 +1,42 @@
1
1
  "use client";
2
2
  import { useQueryClient } from "./provider.mjs";
3
3
  import { unsafeCast } from "./cast.mjs";
4
- import { use, useCallback, useEffect, useState, useSyncExternalStore } from "react";
4
+ import { QueryObserver } from "@raf-query/core";
5
+ import { use, useEffect, useState, useSyncExternalStore } from "react";
5
6
  //#region src/use-query.ts
6
7
  function queryFor(client, factory, input) {
7
8
  const args = [input];
8
9
  return client.getQuery(factory, ...args);
9
10
  }
10
- function useRefetchInterval(query, interval) {
11
- useEffect(() => {
12
- if (interval === void 0 || !Number.isFinite(interval) || interval <= 0) return;
13
- const timer = setInterval(() => {
14
- if (document.visibilityState !== "hidden") query.refetch();
15
- }, interval);
16
- return () => clearInterval(timer);
17
- }, [query, interval]);
18
- }
19
11
  function useQuery(factory, handle, options = {}) {
20
12
  const query = queryFor(useQueryClient(), factory, handle.input);
21
13
  query.read();
14
+ const refetchInterval = options.refetchInterval;
22
15
  const refetchOnReconnect = options.refetchOnReconnect;
23
16
  const refetchOnWindowFocus = options.refetchOnWindowFocus;
24
- const subscribe = useCallback((onChange) => query.subscribe(onChange, {
25
- refetchOnReconnect,
26
- refetchOnWindowFocus
27
- }), [
17
+ const whilePending = options.whilePending;
18
+ const [observer] = useState(() => new QueryObserver(query, options));
19
+ const result = observer.getOptimisticResult(query, options);
20
+ useSyncExternalStore(observer.subscribe, observer.getCurrentResult, observer.getCurrentResult);
21
+ useEffect(() => {
22
+ observer.setOptions(query, {
23
+ refetchInterval,
24
+ refetchOnReconnect,
25
+ refetchOnWindowFocus,
26
+ whilePending
27
+ });
28
+ }, [
29
+ observer,
28
30
  query,
31
+ refetchInterval,
29
32
  refetchOnReconnect,
30
- refetchOnWindowFocus
33
+ refetchOnWindowFocus,
34
+ whilePending
31
35
  ]);
32
- const state = useSyncExternalStore(subscribe, query.getState, query.getState);
33
- useRefetchInterval(query, options.refetchInterval);
34
- const keepPrevious = options.whilePending === "previous";
35
- const [lastShown, setLastShown] = useState(void 0);
36
- if (keepPrevious && state.hasData && lastShown?.promise !== state.promise) setLastShown({
37
- handle: query.handle,
38
- hash: query.hash,
39
- promise: state.promise,
40
- updatedAt: state.updatedAt
41
- });
42
- const previous = keepPrevious && !state.hasData && state.status !== "error" && lastShown !== void 0 && lastShown.hash !== query.hash ? lastShown : void 0;
43
- const shown = previous ?? {
44
- handle: query.handle,
45
- hash: query.hash,
46
- promise: state.promise,
47
- updatedAt: state.updatedAt
48
- };
49
- const controls = {
50
- error: state.error,
51
- fetchNextPage: query.fetchNextPage,
52
- fetchPreviousPage: query.fetchPreviousPage,
53
- hash: query.hash,
54
- hasNextPage: query.hasNextPage(),
55
- hasPreviousPage: query.hasPreviousPage(),
56
- input: shown.handle.input,
57
- isFetching: state.isFetching,
58
- isFetchingNextPage: state.fetchDirection === "forward",
59
- isFetchingPreviousPage: state.fetchDirection === "backward",
60
- isPending: !state.hasData,
61
- isPrevious: previous !== void 0,
62
- isStale: state.isStale,
63
- key: shown.handle.key,
64
- refetch: query.refetch,
65
- updatedAt: shown.updatedAt
66
- };
67
- if (handle.ensured === true) return unsafeCast([use(shown.promise), controls]);
68
- return unsafeCast([shown.promise, controls]);
36
+ observer.trackProp("promise");
37
+ const controls = observer.trackResult(result);
38
+ if (handle.ensured === true) return unsafeCast([use(result.promise), controls]);
39
+ return unsafeCast([result.promise, controls]);
69
40
  }
70
41
  //#endregion
71
42
  export { queryFor, useQuery };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raf-query/react",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
4
4
  "description": "React bindings for raf-query, a render-as-you-fetch query library",
5
5
  "keywords": [
6
6
  "query",
@@ -40,7 +40,7 @@
40
40
  "./tanstack-router": "./dist/tanstack-router.mjs"
41
41
  },
42
42
  "dependencies": {
43
- "@raf-query/core": "0.0.13",
43
+ "@raf-query/core": "0.0.14",
44
44
  "effect": "^4.0.1"
45
45
  },
46
46
  "peerDependencies": {