Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
84c80aa
perf(fetchium): microtask refetches, prefetch/retain leases, useSuspe…
abedell-phantom Oct 3, 2026
1e0b42b
perf(fetchium): skip live value notify when a reducer returns the cur…
abedell-phantom Oct 3, 2026
8ed72a8
perf(fetchium): start prefetch/retain fetches synchronously
abedell-phantom Oct 3, 2026
b053d83
fix(fetchium): do not retry 4xx client errors; add shouldRetry
abedell-phantom Oct 3, 2026
6702797
fix(fetchium): notify a live value after every reducer run again
abedell-phantom Oct 4, 2026
b8528e4
fix(fetchium): suspend on the current fetch when a suspense hold's fe…
abedell-phantom Oct 4, 2026
bc0b555
fix(fetchium): give a remount a new attempt after an abandoned suspen…
abedell-phantom Oct 4, 2026
9003c44
fix(fetchium): resubscribe a never-loaded query before refetching it
abedell-phantom Oct 4, 2026
642baa9
fix(fetchium): bound lease lifetimes and clean them up on destroy
abedell-phantom Oct 4, 2026
86577fa
fix(fetchium): retry every failed attempt by default; keep shouldRetr…
abedell-phantom Oct 5, 2026
887d5f8
chore(fetchium): rewrite this change's changesets for React developers
abedell-phantom Oct 6, 2026
f8cb9ec
Merge branch 'perf/quiet-reactivation' into perf/fast-delivery
abedell-phantom Oct 6, 2026
2dc709e
chore(fetchium): tighten comments
abedell-phantom Oct 7, 2026
7fca8d6
Merge branch 'perf/quiet-reactivation' into perf/fast-delivery
abedell-phantom Oct 7, 2026
aa417f3
fix(fetchium): release a retain() lease when starting one of its quer…
abedell-phantom Oct 7, 2026
10958e9
chore(fetchium): cut and shorten comments
abedell-phantom Oct 7, 2026
15a2722
Merge branch 'perf/quiet-reactivation' into perf/fast-delivery
abedell-phantom Oct 7, 2026
8cccf20
fix(fetchium): keep a throwing getConfig() from stalling Signalium's …
abedell-phantom Oct 7, 2026
cb300b4
fix(fetchium): recover a query once its getConfig() stops throwing
abedell-phantom Oct 7, 2026
181236c
Merge branch 'perf/quiet-reactivation' into perf/fast-delivery
abedell-phantom Oct 7, 2026
27ea7d8
fix(fetchium): cancel any queued debounced run when starting a fetch …
abedell-phantom Oct 7, 2026
a0f6208
fix(fetchium): pass a mutation response's status to shouldRetry
abedell-phantom Oct 7, 2026
29899c7
Merge branch 'perf/quiet-reactivation' into perf/fast-delivery
abedell-phantom Oct 7, 2026
5db8976
Merge fix for review comment into perf/fast-delivery
abedell-phantom Oct 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/fast-delivery-prefetch-retain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'fetchium': minor
---

New `queryClient.prefetch(Query, params, { ttl })` and `queryClient.retain(fn, { ttl })` start queries before any component reads them. A component that mounts while one is held reuses its request and renders the data on its first render if it has arrived. Both return a `release` function; `prefetch` lasts 10 seconds by default and `retain` until released.

New `useSuspenseQuery` in `fetchium/react`. It suspends only while the query has no data yet, and sends a failed first fetch to the error boundary.

- A refetch without `debounce` starts on a microtask instead of after `setTimeout(0)`.
- Reading `.value` on a pending or failed query doesn't suspend or throw. The docs said it did.
7 changes: 7 additions & 0 deletions .changeset/should-retry-hook.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'fetchium': minor
---

New optional `shouldRetry(error, attempt, status)` on `QueryClientConfig` and on a query's or mutation's `retry` config. Return `false` to stop retrying an error you know is permanent, such as a 404.

`status` is the HTTP status when it is known, and `getErrorStatus` is exported. Without the hook, retries work as before. `retry.retries` is now optional.
55 changes: 45 additions & 10 deletions docs/src/app/api/fetchium-react/page.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ description: API reference for the fetchium React integration.
React hooks for using Fetchium queries in React components. Built on top of Signalium's `useReactive` hook.

```ts
import { useQuery } from 'fetchium/react';
import { useQuery, useSuspenseQuery } from 'fetchium/react';
```

---
Expand Down Expand Up @@ -39,14 +39,14 @@ React hook for fetching a query. Subscribes the component to the query's reactiv

The returned promise object has the following properties:

| Property | Type | Description |
| ------------ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `value` | `QueryResult<T>` | The resolved query result. Reading this while pending triggers React Suspense. Returns a deep clone to avoid accidental mutation of cached entities. |
| `isReady` | `boolean` | `true` once the query has loaded a value at least once. Use for type narrowing on `value`. |
| `isPending` | `boolean` | `true` while the query is loading. Also true during refetches, even if a value already exists. |
| `isResolved` | `boolean` | `true` when the most recent execution resolved successfully. |
| `isRejected` | `boolean` | `true` when the most recent execution failed. |
| `error` | `unknown` | The error if `isRejected` is `true`. |
| Property | Type | Description |
| ------------ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `value` | `QueryResult<T>` | The resolved query result, or `undefined` until the first value arrives. Returns a deep clone to avoid accidental mutation of cached entities. |
| `isReady` | `boolean` | `true` once the query has loaded a value at least once. Use for type narrowing on `value`. |
| `isPending` | `boolean` | `true` while the query is loading. Also true during refetches, even if a value already exists. |
| `isResolved` | `boolean` | `true` when the most recent execution resolved successfully. |
| `isRejected` | `boolean` | `true` when the most recent execution failed. |
| `error` | `unknown` | The error if `isRejected` is `true`. |

The resolved `QueryResult<T>` includes pagination helpers:

Expand Down Expand Up @@ -133,4 +133,39 @@ const SearchResults = component(() => {
- `useQuery` is a thin wrapper around Signalium v3's `useReactive`, which is deep-by-default. It returns a **structurally-shared snapshot** of the query result, so memoized children that receive subtrees as props keep stable references when the underlying data is unchanged.
- Fetchium registers a custom snapshot for entity proxies so the snapshot walks into entities (instead of returning them by reference), giving you correct re-rendering on entity field changes.
- The snapshot is a plain-object copy of the query result to prevent accidental mutation of the entity cache. Use `draft()` from `fetchium` if you need a mutable copy for mutations.
- When used with React Suspense, reading `.value` on a pending query will suspend the component.
- `useQuery` never suspends. Reading `.value` while pending returns `undefined`. Use [`useSuspenseQuery`](#usesuspensequery) to suspend on a cold miss.

---

### `useSuspenseQuery`

```ts
function useSuspenseQuery<T extends Query>(
QueryClass: new () => T,
params?: ExtractQueryParams<T>,
): ReadyReactivePromise<QueryResult<T>>;
```

`useQuery` for components under a React `<Suspense>` boundary. It suspends only on a **cold miss**: the query has never produced a value, neither in memory nor, with a synchronous store, in the persisted cache. Anything else renders at once, including a stale value whose refetch is in flight. Refetches never suspend. Because the hook only returns once a value exists, `value` is always defined.

A cold fetch that fails is thrown to the nearest error boundary. Resetting the boundary (remounting the component) tries the fetch again.

Changing params to ones with no value yet is a cold miss, so the component suspends. Wrap the change in `startTransition` to keep showing the previous result.

Call it from a plain function component. Like `useQuery`, it cannot run inside a Signalium `component()`, which is a reactive context.

```tsx
import { Suspense } from 'react';
import { useSuspenseQuery } from 'fetchium/react';

function UserName({ id }: { id: number }) {
const user = useSuspenseQuery(GetUser, { id });
return <span>{user.value.name}</span>;
}

<Suspense fallback={<Spinner />}>
<UserName id={42} />
</Suspense>;
```

While suspended, the client keeps the query active, since React discards a suspended render without subscribing to anything. The component's commit takes over.
Loading
Loading