Skip to content

feat(solid-query): type useQuery/useInfiniteQuery data as non-nullable - #11292

Closed
brenelz wants to merge 6 commits into
TanStack:solid-query-v6-prefrom
brenelz:feat/solid-query-non-nullable-data
Closed

feat(solid-query): type useQuery/useInfiniteQuery data as non-nullable#11292
brenelz wants to merge 6 commits into
TanStack:solid-query-v6-prefrom
brenelz:feat/solid-query-non-nullable-data

Conversation

@brenelz

@brenelz brenelz commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

In the Solid 2.0 port, reading data on a useQuery/useInfiniteQuery result is backed by an async resource: while the query is loading, the component suspends into the nearest <Loading> boundary, so by the time data is actually read during render the value has settled. The types didn't reflect that — data was still TData | undefined, forcing guards that the runtime makes unnecessary.

This PR makes data non-nullable on the public result types:

  • Adds a distributive NonNullableData<TResult, TData> wrapper in types.ts and applies it to UseBaseQueryResult (and therefore UseQueryResult) and UseInfiniteQueryResult. Each status variant of the result union keeps its other discriminants (status, error, ...); only data is narrowed.
  • useQueries results are a plain reactive store with no resource backing — reads never suspend — so its data stays honestly nullable via a local alias.

No runtime changes.

Test updates

  • Type tests updated to the new non-nullable expectations (useQuery.test-d, useInfiniteQuery.test-d, queryOptions.test-d, infiniteQueryOptions.test-d). The maybe-undefined initialData function case keeps | undefined, since there the undefined comes from TData itself through the defined-initialData overload (matching React Query).
  • useQueries.test-d now asserts against the raw query-core QueryObserverResult, since useQueries keeps nullable data.
  • Runtime tests that deliberately observe pending/error states read data before the value exists at runtime; those reads are widened through a new pendingData test helper in __tests__/utils.tsx, which documents the mismatch instead of tripping no-unnecessary-condition or losing runtime-necessary guards.

Verification

  • tsc --build clean
  • eslint ./src clean
  • vitest run: 22 files, 329 tests passed

Reading data on a useQuery/useInfiniteQuery result is backed by an async
resource that suspends the component into the nearest Loading boundary
while the query loads, so by the time data is read during render it has
settled. Reflect that in the types: data is TData, never undefined.

- Add a distributive NonNullableData wrapper and apply it to
  UseBaseQueryResult and UseInfiniteQueryResult, keeping each status
  variant's other discriminants intact.
- useQueries results are a plain reactive store with no resource backing
  (reads never suspend), so keep its data nullable via a local alias.
- Update type tests for the new expectations, and widen intentional
  pending-state observations in runtime tests through a new pendingData
  test helper, which documents that those reads happen before the value
  exists at runtime.
@coderabbitai

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nx-cloud

nx-cloud Bot commented Aug 25, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit daa9052

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 2m 50s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 1s View ↗

☁️ Nx Cloud last updated this comment at 2026-08-26 00:51:13 UTC

@pkg-pr-new

pkg-pr-new Bot commented Aug 25, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-query-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-experimental@11292

@tanstack/eslint-plugin-query

npm i https://pkg.pr.new/@tanstack/eslint-plugin-query@11292

@tanstack/lit-query

npm i https://pkg.pr.new/@tanstack/lit-query@11292

@tanstack/preact-query

npm i https://pkg.pr.new/@tanstack/preact-query@11292

@tanstack/preact-query-devtools

npm i https://pkg.pr.new/@tanstack/preact-query-devtools@11292

@tanstack/preact-query-persist-client

npm i https://pkg.pr.new/@tanstack/preact-query-persist-client@11292

@tanstack/query-async-storage-persister

npm i https://pkg.pr.new/@tanstack/query-async-storage-persister@11292

@tanstack/query-broadcast-client-experimental

npm i https://pkg.pr.new/@tanstack/query-broadcast-client-experimental@11292

@tanstack/query-core

npm i https://pkg.pr.new/@tanstack/query-core@11292

@tanstack/query-devtools

npm i https://pkg.pr.new/@tanstack/query-devtools@11292

@tanstack/query-persist-client-core

npm i https://pkg.pr.new/@tanstack/query-persist-client-core@11292

@tanstack/query-sync-storage-persister

npm i https://pkg.pr.new/@tanstack/query-sync-storage-persister@11292

@tanstack/react-query

npm i https://pkg.pr.new/@tanstack/react-query@11292

@tanstack/react-query-devtools

npm i https://pkg.pr.new/@tanstack/react-query-devtools@11292

@tanstack/react-query-next-experimental

npm i https://pkg.pr.new/@tanstack/react-query-next-experimental@11292

@tanstack/react-query-persist-client

npm i https://pkg.pr.new/@tanstack/react-query-persist-client@11292

@tanstack/solid-query

npm i https://pkg.pr.new/@tanstack/solid-query@11292

@tanstack/solid-query-devtools

npm i https://pkg.pr.new/@tanstack/solid-query-devtools@11292

@tanstack/solid-query-persist-client

npm i https://pkg.pr.new/@tanstack/solid-query-persist-client@11292

@tanstack/svelte-query

npm i https://pkg.pr.new/@tanstack/svelte-query@11292

@tanstack/svelte-query-devtools

npm i https://pkg.pr.new/@tanstack/svelte-query-devtools@11292

@tanstack/svelte-query-persist-client

npm i https://pkg.pr.new/@tanstack/svelte-query-persist-client@11292

@tanstack/vue-query

npm i https://pkg.pr.new/@tanstack/vue-query@11292

@tanstack/vue-query-devtools

npm i https://pkg.pr.new/@tanstack/vue-query-devtools@11292

commit: daa9052

Rework the test call sites to sit closer to the non-nullable data
contract instead of widening reads back to T | undefined:

- Scalar displays read data naked ({state.data} renders empty while
  pending), including the persist-client provider test, which already
  read data naked everywhere else. Assertions on the removed 'null'
  fallbacks now match the bare label.
- Snapshot captures push state.data directly. No test in the infinite
  file uses the reconcile option, so data is replaced rather than
  mutated in place and the JSON deep-copies were redundant. The useQuery
  reconcile test keeps its snapshot() capture, since in-place mutation
  is exactly what it asserts on.
- Reads that dereference data (.pages, indexing) gate on isSuccess. On
  the client the proxy returns the raw store value, so a pending read is
  undefined at runtime and dereferencing it throws a TypeError that
  halts the reactive system - only the isServer branch routes through
  the query resource and throws NotReadyError. Reads already guarded by
  a Switch with a pending Match need no gate, since the fallback is
  never evaluated while pending.
- suspense.test's render effect now tracks its spread in the compute
  function rather than reading state in the callback, clearing the
  STRICT_READ_UNTRACKED warnings that run emitted.
@brenelz
brenelz marked this pull request as draft August 25, 2026 19:26
brenelz and others added 3 commits August 25, 2026 14:37
The non-nullable data type only held on the server. In the result
Proxy, just the isServer branch routed reads through the query
resource (which throws NotReadyError); on the client the Proxy handed
back the raw store value, so a pending data read was undefined and
dereferencing it threw a TypeError that halted the reactive system.

Suspend on the client too: a tracked data read while isLoading throws
NotReadyError to the nearest <Loading> boundary. The state reads in the
check re-subscribe the reader, so it re-runs when the subscriber syncs
the settled result. Only isLoading suspends - a pending-but-idle query
(disabled, or reset with nothing in flight) still yields undefined
rather than parking the boundary forever. Untracked reads pass through,
so event handlers and effect callbacks can peek without suspending.

This also fixes a hydration bug: the server suspended and rendered the
resolved markup while the client rendered a pending pass and appended a
second copy instead of claiming it, duplicating server-rendered lists.
Both sides now suspend alike and hydration claims the markup.

Tests read data naked again, with no status gates:

- Effects that tracked the whole result via { ...state } now track
  deep(state), which subscribes to every property without routing data
  through the suspending read, so pending states stay observable and
  every existing assertion holds unchanged.
- Narrow effects that tracked data now track dataUpdatedAt, keeping the
  same re-run points; the one effect that pushes its own computed
  record reads data through untrack to keep that record intact.
- Guards on data inside tracking scopes are gone, since a tracked read
  can no longer observe undefined.
Suspending a tracked data read while Solid is claiming server-rendered
DOM (sharedConfig.hydrating) bails the claim: the server rendered this
content from settled data that the streaming hydration channel may not
have primed on the client yet, so the throw leaves unclaimed server
nodes and crashes the reactive system with 'Potential Infinite Loop
Detected'. Reads during the hydration window return the store value
instead — exactly the pre-suspense behavior — and the per-query
hydration coordinator re-syncs them once their entry lands.

Reproduced in a fullstack streaming-SSR testbed (queries still in
flight at hydration time): crashes on every affected load without the
guard, zero errors across repeated loads with it. 329/329 package
tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@brenelz

brenelz commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator Author

Superceeded by this pr
#11308

@brenelz brenelz closed this Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant