Skip to content

ref: switch from throwing promises to React.use#10366

Open
TkDodo wants to merge 11 commits intomainfrom
feature/use
Open

ref: switch from throwing promises to React.use#10366
TkDodo wants to merge 11 commits intomainfrom
feature/use

Conversation

@TkDodo
Copy link
Copy Markdown
Collaborator

@TkDodo TkDodo commented Mar 31, 2026

Summary by CodeRabbit

  • New Features

    • Exposed a public helper for pending thenables to support suspense coordination.
    • Added a new example integration (React Start) with routing and SSR-ready profiles page.
  • Improvements

    • More reliable Suspense behavior and coordinated multi-query suspense handling with cached promises.
    • Better SSR/hydration for suspenseful queries and more predictable retry/reset flows.
  • Tests

    • Tests updated to drive Suspense and interactions asynchronously using new helpers.

@nx-cloud
Copy link
Copy Markdown

nx-cloud bot commented Mar 31, 2026

View your CI Pipeline Execution ↗ for commit a5fec0f

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

☁️ Nx Cloud last updated this comment at 2026-03-31 18:21:48 UTC

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 31, 2026

🚀 Changeset Version Preview

No changeset entries found. Merging this PR will not cause a version bump for any packages.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 31, 2026

More templates

@tanstack/angular-query-experimental

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

@tanstack/eslint-plugin-query

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

@tanstack/preact-query

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

@tanstack/preact-query-devtools

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

@tanstack/preact-query-persist-client

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

@tanstack/query-async-storage-persister

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

@tanstack/query-broadcast-client-experimental

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

@tanstack/query-core

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

@tanstack/query-devtools

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

@tanstack/query-persist-client-core

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

@tanstack/query-sync-storage-persister

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

@tanstack/react-query

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

@tanstack/react-query-devtools

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

@tanstack/react-query-next-experimental

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

@tanstack/react-query-persist-client

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

@tanstack/solid-query

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

@tanstack/solid-query-devtools

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

@tanstack/solid-query-persist-client

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

@tanstack/svelte-query

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

@tanstack/svelte-query-devtools

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

@tanstack/svelte-query-persist-client

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

@tanstack/vue-query

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

@tanstack/vue-query-devtools

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

commit: a5fec0f

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 31, 2026

size-limit report 📦

Path Size
react full 12.23 KB (+2.05% 🔺)
react minimal 9.15 KB (+1.51% 🔺)

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 31, 2026

📝 Walkthrough

Walkthrough

Adds suspense orchestration: exports a pending thenable, introduces a React-compatible use fallback, implements cached suspense promises, updates hooks to use the new promise flow, and converts many tests to async suspense-aware helpers and act-wrapped interactions.

Changes

Cohort / File(s) Summary
Query Core Exports
packages/query-core/src/index.ts
Added re-export: pendingThenable from ./thenable.
Suspense Core
packages/react-query/src/suspense.ts
Introduced use (React.use or fallback), getSuspensePromise, and a module-level WeakMap cache; adjusted fetchOptimistic to rethrow after clearing reset and to always call observer.updateResult() in finally.
Hook Integrations
packages/react-query/src/useBaseQuery.ts, packages/react-query/src/useQueries.ts
Switched suspense handling to use getSuspensePromise + use(...) instead of throwing promises; useQueries builds per-query queryHash promises, composes a combined suspense thenable, caches by QueriesObserver, and uses pendingThenable to bridge Promise.all.
Test Utilities
packages/react-query/src/__tests__/utils.tsx
Added async helpers: renderWithClientAndSuspense and rerenderWithSuspense that run renders/rerenders inside act(async ...) and return awaited results.
Tests — Suspense & Queries
packages/react-query/src/__tests__/suspense.test.tsx, packages/react-query/src/__tests__/useSuspenseQuery.test.tsx, packages/react-query/src/__tests__/useSuspenseQueries.test.tsx, packages/react-query/src/__tests__/useSuspenseInfiniteQuery.test.tsx
Converted renders to await renderWithClientAndSuspense/rerenderWithSuspense, replaced some callback-based assertions with DOM assertions, and wrapped user interactions in await act(async () => ...).
Tests — Prefetch & Reset Boundary
packages/react-query/src/__tests__/usePrefetchQuery.test.tsx, packages/react-query/src/__tests__/usePrefetchInfiniteQuery.test.tsx, packages/react-query/src/__tests__/QueryResetErrorBoundary.test.tsx
Updated renders to suspense-aware async helpers and wrapped click interactions in await act(async () => ...); removed some suspense-specific tests/hooks.
Tests — SSR / Hydration
packages/react-query/src/__tests__/ssr.test.tsx, packages/react-query/src/__tests__/ssr-hydration.test.tsx
Added server-side suspension tests using useSuspenseQuery inside React.Suspense and a hydration test asserting no console errors and correct client markup.
Integrations — React Start
integrations/react-start/* (.gitignore, package.json, src/components/*, src/router.tsx, src/routes/*, tsconfig.json, vite.config.ts)
New React Start integration: scaffolds router, routes, components (DefaultCatchBoundary, NotFound), Vite config, tsconfig, and package manifest; router wires QueryClient into TanStack Router and sets up SSR integration.

Sequence Diagram(s)

sequenceDiagram
    participant Component
    participant useBaseQuery
    participant SuspenseModule as use/getSuspensePromise
    participant QueryObserver
    participant WeakMapCache as WeakMapCache

    Component->>useBaseQuery: calls hook (shouldSuspend=true)
    useBaseQuery->>SuspenseModule: request thenable via getSuspensePromise(options, observer, errorResetBoundary)
    SuspenseModule->>WeakMapCache: lookup by observer & queryHash
    alt cached
        WeakMapCache-->>SuspenseModule: return cached thenable
    else not cached
        SuspenseModule->>QueryObserver: call fetchOptimistic(...)
        QueryObserver-->>SuspenseModule: returns promise
        SuspenseModule->>WeakMapCache: store {queryHash, promise}
    end
    SuspenseModule-->>useBaseQuery: returns thenable to React.use / fallback
    Note right of Component: React Suspense boundary awaits thenable
    QueryObserver->>QueryObserver: observer.updateResult() (in finally)
    QueryObserver-->>Component: data available -> render resumes
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • theVedanta

Poem

🐰 I hopped through promises, caches, and use,

Thenables lined up without any bruise,
Tests learned to wait in patient array,
SSR and hydration greet the new day,
A rabbit’s small cheer for this tidy queue.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is completely empty; no description or checklist items were provided by the author. Add a comprehensive description following the template, including what changes were made, why they were made, confirmation of testing, and whether a changeset was generated.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'ref: switch from throwing promises to React.use' clearly and specifically describes the main architectural change in the pull request: migrating from throwing promises for suspense to using React.use hook.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/use

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

@TkDodo TkDodo marked this pull request as ready for review March 31, 2026 17:05
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/react-query/src/__tests__/useSuspenseQueries.test.tsx (1)

696-710: Consider simplifying the microtask flush pattern.

The await Promise.resolve() inside act is used to flush microtasks after advancing timers. While this works, it's an unusual pattern that may confuse future maintainers.

This may be necessary due to the interaction between fake timers, suspense, and placeholderData, so keeping it is acceptable if tests fail without it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-query/src/__tests__/useSuspenseQueries.test.tsx` around lines
696 - 710, The test uses an inline microtask flush via awaiting
Promise.resolve() inside act after vi.advanceTimersByTimeAsync, which is
nonstandard and confusing; replace those inline awaits with a clear utility
(e.g., a shared flushPromises/flushMicrotasks helper) or use the test runner's
explicit microtask helper so intent is clear. Update the two occurrences inside
the act blocks that follow vi.advanceTimersByTimeAsync (around the
act/vi.advanceTimersByTimeAsync/await Promise.resolve sequence) so they call the
chosen helper instead, keeping the rest of the sequence (act,
vi.advanceTimersByTimeAsync, then flush helper) identical and ensuring
fireEvent.click and subsequent assertions still run inside act as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/react-query/src/suspense.ts`:
- Around line 135-153: The cache suspenseObserverPromiseCache stores promises
keyed by observer + defaultedOptions.queryHash but never clears them, causing
reuse of already-settled promises; update the logic around where you create and
set promise (the fetchOptimistic call and suspenseObserverPromiseCache.set) to
attach finally handlers that remove the cache entry for that observer when the
promise settles (both resolve and reject) so future suspensions for the same
observer+queryHash will re-fetch; apply the same change to the other similar
block that uses suspenseObserverPromiseCache (the second occurrence handling
observers).

In `@packages/react-query/src/useQueries.ts`:
- Around line 349-385: The cached combined thenable in
suspenseQueriesPromiseCache can remain fulfilled and should be invalidated once
the Promise.all(...) settles; update getCombinedSuspensePromise so that after
creating the pendingThenable() and wiring
Promise.all(suspensePromises).then(promise.resolve, promise.reject) you also
attach a cleanup step (e.g., a .then or .finally) that removes the cache entry
for this QueriesObserver (suspenseQueriesPromiseCache.delete(observer)) if the
stored promise is the same one you created, ensuring future suspension cycles
recreate a fresh thenable; keep early return for single suspense promise
unchanged and use the existing identifiers suspenseQueriesPromiseCache,
getCombinedSuspensePromise, pendingThenable, and QueriesObserver to locate the
change.

---

Nitpick comments:
In `@packages/react-query/src/__tests__/useSuspenseQueries.test.tsx`:
- Around line 696-710: The test uses an inline microtask flush via awaiting
Promise.resolve() inside act after vi.advanceTimersByTimeAsync, which is
nonstandard and confusing; replace those inline awaits with a clear utility
(e.g., a shared flushPromises/flushMicrotasks helper) or use the test runner's
explicit microtask helper so intent is clear. Update the two occurrences inside
the act blocks that follow vi.advanceTimersByTimeAsync (around the
act/vi.advanceTimersByTimeAsync/await Promise.resolve sequence) so they call the
chosen helper instead, keeping the rest of the sequence (act,
vi.advanceTimersByTimeAsync, then flush helper) identical and ensuring
fireEvent.click and subsequent assertions still run inside act as before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: be132759-0ec4-4901-a690-8e2a45b28e29

📥 Commits

Reviewing files that changed from the base of the PR and between 3fd3460 and 1392061.

📒 Files selected for processing (12)
  • packages/query-core/src/index.ts
  • packages/react-query/src/__tests__/QueryResetErrorBoundary.test.tsx
  • packages/react-query/src/__tests__/suspense.test.tsx
  • packages/react-query/src/__tests__/usePrefetchInfiniteQuery.test.tsx
  • packages/react-query/src/__tests__/usePrefetchQuery.test.tsx
  • packages/react-query/src/__tests__/useSuspenseInfiniteQuery.test.tsx
  • packages/react-query/src/__tests__/useSuspenseQueries.test.tsx
  • packages/react-query/src/__tests__/useSuspenseQuery.test.tsx
  • packages/react-query/src/__tests__/utils.tsx
  • packages/react-query/src/suspense.ts
  • packages/react-query/src/useBaseQuery.ts
  • packages/react-query/src/useQueries.ts

Comment on lines +135 to +153
const queryHash = defaultedOptions.queryHash
const cached = suspenseObserverPromiseCache.get(observer)

if (cached?.queryHash === queryHash) {
return cached.promise as Promise<QueryObserverResult<TData, TError>>
}

const promise = fetchOptimistic(
defaultedOptions,
observer,
errorResetBoundary,
)

suspenseObserverPromiseCache.set(observer, {
queryHash,
promise,
})

return promise
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Expire cached suspense promises after they settle.

This cache only keys on queryHash and never clears. If the same observer suspends again for the same key—e.g. after resetQueries, an error-boundary retry, or another same-key cycle that goes back to isPendinguse() will reuse a fulfilled promise and stop suspending, so the hook can leak a pending result instead of the fallback.

🛠️ Minimal fix
   const promise = fetchOptimistic(
     defaultedOptions,
     observer,
     errorResetBoundary,
   )

   suspenseObserverPromiseCache.set(observer, {
     queryHash,
     promise,
   })
+
+  void promise.finally(() => {
+    const cached = suspenseObserverPromiseCache.get(observer)
+    if (cached?.promise === promise) {
+      suspenseObserverPromiseCache.delete(observer)
+    }
+  })

   return promise
 }

Also applies to: 156-162

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-query/src/suspense.ts` around lines 135 - 153, The cache
suspenseObserverPromiseCache stores promises keyed by observer +
defaultedOptions.queryHash but never clears them, causing reuse of
already-settled promises; update the logic around where you create and set
promise (the fetchOptimistic call and suspenseObserverPromiseCache.set) to
attach finally handlers that remove the cache entry for that observer when the
promise settles (both resolve and reject) so future suspensions for the same
observer+queryHash will re-fetch; apply the same change to the other similar
block that uses suspenseObserverPromiseCache (the second occurrence handling
observers).

Comment on lines +349 to +385
const suspenseQueriesPromiseCache = new WeakMap<
QueriesObserver<any>,
{
queryHashes: Array<string>
promise: Promise<Array<unknown>>
}
>()

function getCombinedSuspensePromise(
observer: QueriesObserver<any>,
queryHashes: Array<string>,
suspensePromises: Array<Promise<unknown>>,
) {
if (suspensePromises.length === 1) {
return suspensePromises[0]!
}

const cached = suspenseQueriesPromiseCache.get(observer)

if (
cached &&
cached.queryHashes.length === queryHashes.length &&
cached.queryHashes.every((hash, index) => hash === queryHashes[index])
) {
return cached.promise
}

const promise = pendingThenable<Array<unknown>>()

Promise.all(suspensePromises).then(promise.resolve, promise.reject)

suspenseQueriesPromiseCache.set(observer, {
queryHashes,
promise,
})

return promise
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Invalidate the combined thenable once a suspense cycle finishes.

This cache is sticky across cycles. After the first Promise.all(...) settles, a later same-hash suspension on the same QueriesObserver will reuse a fulfilled thenable, so use() won’t suspend anymore and useQueries can leak pending results on reset/retry flows.

🛠️ Minimal fix
   const promise = pendingThenable<Array<unknown>>()

-  Promise.all(suspensePromises).then(promise.resolve, promise.reject)
+  Promise.all(suspensePromises).then(
+    (value) => {
+      const cached = suspenseQueriesPromiseCache.get(observer)
+      if (cached?.promise === promise) {
+        suspenseQueriesPromiseCache.delete(observer)
+      }
+      promise.resolve(value)
+    },
+    (error) => {
+      const cached = suspenseQueriesPromiseCache.get(observer)
+      if (cached?.promise === promise) {
+        suspenseQueriesPromiseCache.delete(observer)
+      }
+      promise.reject(error)
+    },
+  )

   suspenseQueriesPromiseCache.set(observer, {
     queryHashes,
     promise,
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-query/src/useQueries.ts` around lines 349 - 385, The cached
combined thenable in suspenseQueriesPromiseCache can remain fulfilled and should
be invalidated once the Promise.all(...) settles; update
getCombinedSuspensePromise so that after creating the pendingThenable() and
wiring Promise.all(suspensePromises).then(promise.resolve, promise.reject) you
also attach a cleanup step (e.g., a .then or .finally) that removes the cache
entry for this QueriesObserver (suspenseQueriesPromiseCache.delete(observer)) if
the stored promise is the same one you created, ensuring future suspension
cycles recreate a fresh thenable; keep early return for single suspense promise
unchanged and use the existing identifiers suspenseQueriesPromiseCache,
getCombinedSuspensePromise, pendingThenable, and QueriesObserver to locate the
change.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
packages/react-query/src/suspense.ts (1)

144-159: ⚠️ Potential issue | 🔴 Critical

Expire cached suspense promises after settlement.

Line 144 returns cached promises solely by observer + queryHash, but Line 154 only sets entries and never clears them. A settled promise can be reused in a later pending cycle, skipping intended suspension/fetch behavior.

Minimal fix
   const promise = fetchOptimistic(
     defaultedOptions,
     observer,
     errorResetBoundary,
   )

   suspenseObserverPromiseCache.set(observer, {
     queryHash,
     promise,
   })
+
+  void promise.finally(() => {
+    const cached = suspenseObserverPromiseCache.get(observer)
+    if (cached?.promise === promise) {
+      suspenseObserverPromiseCache.delete(observer)
+    }
+  })

   return promise
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-query/src/suspense.ts` around lines 144 - 159, The cached
suspense promise stored in suspenseObserverPromiseCache for an
observer/queryHash pair is never cleared, so settled promises can be reused
incorrectly; update the logic where you set the cache (in the code that creates
promise via fetchOptimistic and calls suspenseObserverPromiseCache.set(observer,
{ queryHash, promise })) to attach a finally handler to the promise that removes
the cache entry (only if the stored entry still matches the same queryHash) so
the cache only holds in-flight promises and is cleared after resolution or
rejection (reference: suspenseObserverPromiseCache, observer, queryHash, and the
promise returned from fetchOptimistic).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/react-query/src/suspense.ts`:
- Around line 144-159: The cached suspense promise stored in
suspenseObserverPromiseCache for an observer/queryHash pair is never cleared, so
settled promises can be reused incorrectly; update the logic where you set the
cache (in the code that creates promise via fetchOptimistic and calls
suspenseObserverPromiseCache.set(observer, { queryHash, promise })) to attach a
finally handler to the promise that removes the cache entry (only if the stored
entry still matches the same queryHash) so the cache only holds in-flight
promises and is cleared after resolution or rejection (reference:
suspenseObserverPromiseCache, observer, queryHash, and the promise returned from
fetchOptimistic).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2f7bb92c-d9bc-407c-b885-8448f4c43feb

📥 Commits

Reviewing files that changed from the base of the PR and between 1392061 and 8510102.

📒 Files selected for processing (1)
  • packages/react-query/src/suspense.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/react-query/src/__tests__/ssr.test.tsx (1)

242-266: Strengthen the assertion to verify the “no undefined data” contract

The title says undefined data must not leak, but the test currently only checks for "loading". Add a negative assertion for "undefined" so the intent is explicitly enforced.

Suggested patch
     expect(markup).toContain('loading')
+    expect(markup).not.toContain('undefined')
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-query/src/__tests__/ssr.test.tsx` around lines 242 - 266, The
test titled "useSuspenseQuery should suspend on the server instead of exposing
undefined data" currently only asserts that the fallback "loading" is present;
add a negative assertion to ensure undefined data doesn't leak by asserting that
the rendered markup does not contain the string "undefined" (e.g.,
expect(markup).not.toContain('undefined')) after renderToString in the same test
that uses useSuspenseQuery, Page, queryFn and markup so the intent is explicitly
enforced.
packages/react-query/src/__tests__/ssr-hydration.test.tsx (1)

311-329: Make hydration assertions explicit for both fallback and undefined leakage

toContain('1') is a good signal, but this test’s contract is stronger. Add explicit negative checks for "loading" and "undefined" on both server markup and hydrated DOM.

Suggested patch
     expect(markup).toContain('1')
+    expect(markup).not.toContain('loading')
+    expect(markup).not.toContain('undefined')
@@
     expect(consoleMock).toHaveBeenCalledTimes(0)
     expect(el.innerHTML).toContain('1')
+    expect(el.innerHTML).not.toContain('loading')
+    expect(el.innerHTML).not.toContain('undefined')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-query/src/__tests__/ssr-hydration.test.tsx` around lines 311 -
329, The test currently only asserts that the server markup and hydrated DOM
contain '1' but doesn't assert that the fallback ("loading") or stringified
"undefined" leaked; update the assertions around markup and el.innerHTML (after
creating markup/stringifiedState and after hydrate + ReactHydrate with
QueryClient and ProfilesComponent) to explicitly assert that neither 'loading'
nor 'undefined' appear (e.g., expect(markup).not.toContain('loading') and
.not.toContain('undefined'), and expect(el.innerHTML).not.toContain('loading')
and .not.toContain('undefined')), keeping the existing consoleMock and '1'
assertions intact so the test verifies no fallback/undefined leakage during
server render or hydration.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/react-query/src/__tests__/ssr-hydration.test.tsx`:
- Around line 311-329: The test currently only asserts that the server markup
and hydrated DOM contain '1' but doesn't assert that the fallback ("loading") or
stringified "undefined" leaked; update the assertions around markup and
el.innerHTML (after creating markup/stringifiedState and after hydrate +
ReactHydrate with QueryClient and ProfilesComponent) to explicitly assert that
neither 'loading' nor 'undefined' appear (e.g.,
expect(markup).not.toContain('loading') and .not.toContain('undefined'), and
expect(el.innerHTML).not.toContain('loading') and .not.toContain('undefined')),
keeping the existing consoleMock and '1' assertions intact so the test verifies
no fallback/undefined leakage during server render or hydration.

In `@packages/react-query/src/__tests__/ssr.test.tsx`:
- Around line 242-266: The test titled "useSuspenseQuery should suspend on the
server instead of exposing undefined data" currently only asserts that the
fallback "loading" is present; add a negative assertion to ensure undefined data
doesn't leak by asserting that the rendered markup does not contain the string
"undefined" (e.g., expect(markup).not.toContain('undefined')) after
renderToString in the same test that uses useSuspenseQuery, Page, queryFn and
markup so the intent is explicitly enforced.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8205bbb8-f670-4446-b0ae-1368d44600a6

📥 Commits

Reviewing files that changed from the base of the PR and between 8510102 and 4315a70.

📒 Files selected for processing (2)
  • packages/react-query/src/__tests__/ssr-hydration.test.tsx
  • packages/react-query/src/__tests__/ssr.test.tsx

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
integrations/react-start/src/components/default-catch-boundary.tsx (1)

38-49: Minor: "Go Back" link has mismatched to prop and actual behavior.

The link has to="/" but the onClick handler prevents default navigation and calls window.history.back(). This could cause confusion if inspecting the component and may not work correctly with JS disabled. Consider using a <button> element instead since this doesn't perform standard link navigation.

♻️ Suggested refactor to use button for history.back()
        ) : (
-          <Link
-            to="/"
-            className={`px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded-sm text-white uppercase font-extrabold`}
-            onClick={(e) => {
-              e.preventDefault()
-              window.history.back()
-            }}
-          >
+          <button
+            onClick={() => window.history.back()}
+            className={`px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded-sm text-white uppercase font-extrabold`}
+          >
             Go Back
-          </Link>
+          </button>
         )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@integrations/react-start/src/components/default-catch-boundary.tsx` around
lines 38 - 49, The "Go Back" control uses a Link with a conflicting to="/" prop
while its onClick prevents navigation and calls window.history.back(), which is
misleading and problematic; replace the Link component with a semantic <button>
(or the framework's Button component) in the default-catch-boundary so it no
longer supplies a to prop, remove the preventDefault logic, keep the existing
className for styling, ensure the onClick calls window.history.back() (or
history.back()) and add an accessible label/aria-label if needed; update any
references to Link in this block and confirm the handler is defined inline on
the button.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@integrations/react-start/src/components/default-catch-boundary.tsx`:
- Around line 38-49: The "Go Back" control uses a Link with a conflicting to="/"
prop while its onClick prevents navigation and calls window.history.back(),
which is misleading and problematic; replace the Link component with a semantic
<button> (or the framework's Button component) in the default-catch-boundary so
it no longer supplies a to prop, remove the preventDefault logic, keep the
existing className for styling, ensure the onClick calls window.history.back()
(or history.back()) and add an accessible label/aria-label if needed; update any
references to Link in this block and confirm the handler is defined inline on
the button.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d9e9158a-3f6e-4c1a-9c94-bda392ddb982

📥 Commits

Reviewing files that changed from the base of the PR and between 4315a70 and a5fec0f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • integrations/react-start/.gitignore
  • integrations/react-start/package.json
  • integrations/react-start/src/components/default-catch-boundary.tsx
  • integrations/react-start/src/components/not-found.tsx
  • integrations/react-start/src/router.tsx
  • integrations/react-start/src/routes/__root.tsx
  • integrations/react-start/src/routes/index.tsx
  • integrations/react-start/tsconfig.json
  • integrations/react-start/vite.config.ts
✅ Files skipped from review due to trivial changes (3)
  • integrations/react-start/.gitignore
  • integrations/react-start/tsconfig.json
  • integrations/react-start/package.json

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.

1 participant