Skip to content

feat: select, structural sharing, throwOnError and useIsFetching - #151

Merged
prc5 merged 4 commits into
mainfrom
feat/select-structural-sharing
Oct 26, 2026
Merged

prc5 merged 4 commits into
mainfrom
feat/select-structural-sharing

Conversation

@prc5

@prc5 prc5 commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

Closes #150

What

select option for useFetch, useCache, useSubmit and useListener. It runs at the observer level: the hook re-renders only when the selected value changes, while the request and its cache stay shared between hooks.

const { data: price } = useFetch(getSymbols, { select: (symbols) => symbols[id]?.price });
const { data: symbol } = useListener(symbolsListener, { select: (symbols) => symbols[id] });
  • The selector can close over props. When it changes between renders (new id), it is re-applied to the current data in the same render.
  • data and setData take the selected type, which is inferred. Callbacks, submit() and the cache keep the full response.
  • A selector that throws is surfaced to the nearest Error Boundary and never thrown inside the cache or socket emitters.
  • select cannot be set in the Provider config (a distributive Omit keeps the bounce union intact).

Structural sharing (TanStack-style)

  • Core: cache.set merges incoming data into the previous entry with replaceEqualDeep. A deeply equal response keeps the same data reference across every hook, and unchanged branches are reused. It is on by default and can be turned off per request with the structuralSharing option, setStructuralSharing() or the sdk config.
  • Hooks: selected results are shared too, and state values compared as equal keep their previous reference. Before this, deepCompare skipped the render but still swapped in the new object, so the next unrelated render handed out a fresh reference.

throwOnError on useFetch (boolean | (error) => boolean) throws settled errors to the Error Boundary. It doesn't throw for errors on attempts that will be retried, canceled requests, or offline re-queued ones. Suspense mode is supported too.

useIsFetching(client, filters?) / useIsSubmitting(client, filters?) count running requests, filtered by queryKey (string or RegExp) or a predicate. They are backed by the new dispatcher events onRunningRequestsChange / onRunningRequestsChangeByKey. All running-request mutations now go through a single setter.

Behaviour changes to note

  • useListener / useEmitter now apply the documented default dependencyTracking: true. Before, the default was never applied, so they re-rendered on every timestamp/extra change, which would have defeated select.
  • With deepCompare: false, values that are identical by Object.is no longer trigger a re-render.
  • Without select, useListener still updates data on every message, because messages are events.

Tests

  • core: replaceEqualDeep units, cache sharing (identical / partial / update / opt-out), request setter round-trip, dispatcher running events (add/finish/cancel/clear/no-op)
  • react: select specs for all four hooks (render counts, reference stability, closure change, initialResponse, suspense, setData, selector errors), structural sharing, throwOnError (predicate, retries, boundary reset, suspense), useIsFetching/useIsSubmitting, StrictMode cases, and type-inference specs
  • deflaked use-fetch.refresh.spec (fixed sleeps → waitFor), seen failing once under full-suite load
  • New/changed async specs: 0 failures in 10 repeated runs. Full suites: core 773, react 492, sockets 400 passing. Lint and typecheck clean; core and react build.

🤖 Generated with Claude Code

prc5 and others added 4 commits October 26, 2026 11:49
- cache.set merges new data with the previous entry using replaceEqualDeep, so deeply equal
  responses keep their data reference and unchanged branches are reused
- new structuralSharing request option (default true) with a setStructuralSharing setter
  and sdk configuration
- dispatcher emits onRunningRequestsChange(ByKey) whenever requests start or stop running

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hing

- select option for useFetch, useCache, useSubmit and useListener - the hook rerenders only
  when the selected value changes, while the cache stays shared
- selected data is structurally shared and equal state values keep their previous references
- throwOnError option for useFetch passes settled errors to the closest Error Boundary
- useIsFetching and useIsSubmitting hooks count the running requests with optional filters
- useListener and useEmitter apply the documented dependencyTracking default (true)

Closes #150

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…etching

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@prc5
prc5 requested a review from GerasNyx as a code owner October 26, 2026 10:50
@prc5
prc5 merged commit 125a0a0 into main Oct 26, 2026
4 checks passed
@prc5
prc5 deleted the feat/select-structural-sharing branch October 26, 2026 10:58
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 9.3.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Add "select" support to "useFetch", "useCache", and "useListener"

1 participant