fix(tanstack-router): remove Suspense gate around RouterProvider during hydration#3213
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughRemoves the Suspense-based route-chunk preload gate and its settled tracking. Hydration now renders Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant AppWrapper as AppWrapper/Render
participant RouterProvider as Router
participant ChunkPreloader as RouteChunkPreload
Client->>AppWrapper: render (if provided)
AppWrapper->>Router: render RouterProvider
Note right of Router: no Suspense/gate present
Client->>ChunkPreloader: ensure routeChunkPreloadPromiseRef
Client->>Client: runPostHydrationLoad()
Client->>ChunkPreloader: await routeChunkPreloadPromiseRef
ChunkPreloader-->>Client: preload promise settles
Client->>Router: call router.load()
Router-->>Client: navigation proceeds
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Greptile SummaryThis PR removes the The removal is safe: Confidence Score: 5/5Safe to merge — the fix correctly aligns client and server render trees with no regressions to chunk preloading logic. Single-file change with a clear, well-documented rationale. The server tree shape is confirmed in serverRender.ts (line 22). The removed gate code was the source of the mismatch; the only ref it depended on (routeChunkPreloadSettledRef) is cleanly removed. The remaining routeChunkPreloadPromiseRef is still awaited in the post-hydration effect, preserving the chunk preload sequencing contract. No logic errors or regressions found. No files require special attention. Important Files Changed
Sequence DiagramsequenceDiagram
participant Server as serverRender.ts
participant Client as clientHydrate.ts
participant React as React Hydration
Note over Server: SSR render
Server->>React: AppWrapper > RouterProvider
Note over Client: Before fix (mismatch)
Client-->>React: AppWrapper > Suspense > RouteChunkPreloadGate > RouterProvider
React--xReact: Tree shape mismatch → full client re-render
Note over Client: After fix (matches server)
Client->>React: AppWrapper > RouterProvider
React->>React: Hydration succeeds
Note over Client: Post-hydration effect
Client->>Client: await routeChunkPreloadPromiseRef (chunks)
Client->>Client: router.load() → client nav enabled
Reviews (1): Last reviewed commit: "fix(tanstack-router): remove Suspense ga..." | Re-trigger Greptile |
Code ReviewOverviewThis PR removes the What's correct
Concerns1. No regression test for the Suspense-shape hydration mismatch (minor) The existing test suite verifies that 2. Lost Suspense-gate semantics for post-hydration re-renders (acknowledged trade-off) The old gate blocked React from committing any re-render triggered between mount and chunk-preload completion. With the gate gone, if something triggers a synchronous re-render in that window, route components could render before their chunks are fully loaded. In practice this window is negligible (the effect fires immediately after mount, and 3. Test plan gap The test plan verifies that existing tests pass but does not include a test for "chunk preloads are awaited before VerdictApprove with minor suggestions. The fix is correct, the removed code was doing more harm than good (the |
size-limit report 📦
|
Code ReviewOverviewSolid fix for a genuine hydration bug. Removing the IssuesMissing CHANGELOG entryPer the repo's changelog guidelines, user-visible bug fixes must be recorded in Behavioral change lacks dedicated test coverageThe comment at lines 376–380 correctly documents the semantic difference: React can no longer suspend intermediate re-renders during the brief window between when A straightforward test would: mock Positive notes
|
Code ReviewOverviewThis PR correctly fixes a React hydration mismatch in The core logic is sound:
IssuesMissing test: chunk preload awaited before There is a test verifying that it('waits for chunk preload promise before triggering post-hydration router.load', async () => {
const router = buildRouter();
let resolveChunkLoad: (() => void) | undefined;
router.loadRouteChunk = jest.fn().mockReturnValue(
new Promise<void>((resolve) => { resolveChunkLoad = resolve; }),
);
router.looseRoutesById = { '/products': { id: '/products' } };
router.load = jest.fn().mockResolvedValue(undefined);
// ... render, assert router.load not called, resolve, assert router.load called once
});Inline commentsThree inline notes on the new comments added — they're written in a backward-looking style (explaining what was removed) that reads awkwardly once the PR context is gone. Suggestions in the thread to trim them to the forward-looking contract only. SummaryThe fix is correct and addresses a real SSR regression. The two items above are non-blocking but worth addressing before merge: adding the missing async test and tidying the comments. |
Code ReviewOverviewThis PR fixes a React hydration mismatch in Correctness ✅The fix is correct. Even though Rendering Behavior change: Suspense fallback → deferred
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c835ce72ea
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Code ReviewOverviewThis PR removes the What's Good
IssuesMissing CHANGELOG entryPer the project's Fragile microtask-counting in the chunk-preload timing test
Test data typo: leading space in dehydrated match IDIn the Minor Observations
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/react-on-rails-pro/tests/tanstackRouter.test.ts (1)
1376-1382: Optional: dangling pending promise on early-failure paths.
loadRouteChunkreturns aPromisewhoseresolveis captured intoresolveChunk; if an assertion fails before line 1438, the promise never resolves andrunPostHydrationLoadstays pending until the test finishes. Jest will still pass/fail correctly, but you may want a smallafterEachthat callsresolveChunk?.()(or wrap the whole body in try/finally that resolves it) to avoid noisy unhandled-promise warnings in CI on regressions. Non-blocking nit.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/react-on-rails-pro/tests/tanstackRouter.test.ts` around lines 1376 - 1382, The test creates a Promise whose resolver is captured in resolveChunk by the mocked loadRouteChunk and may remain unresolved on early assertion failures, leaving runPostHydrationLoad pending; ensure the resolver is always invoked on tear-down by adding an afterEach that calls resolveChunk?.() (or wrap the test body in try/finally and call resolveChunk?.() in finally) so the pending promise is resolved and avoids unhandled-promise warnings; refer to resolveChunk, loadRouteChunk and runPostHydrationLoad when making this change.
🤖 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-on-rails-pro/tests/tanstackRouter.test.ts`:
- Around line 1376-1382: The test creates a Promise whose resolver is captured
in resolveChunk by the mocked loadRouteChunk and may remain unresolved on early
assertion failures, leaving runPostHydrationLoad pending; ensure the resolver is
always invoked on tear-down by adding an afterEach that calls resolveChunk?.()
(or wrap the test body in try/finally and call resolveChunk?.() in finally) so
the pending promise is resolved and avoids unhandled-promise warnings; refer to
resolveChunk, loadRouteChunk and runPostHydrationLoad when making this change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 1490e1ed-6ccc-4f62-9415-47af2780aa47
📒 Files selected for processing (1)
packages/react-on-rails-pro/tests/tanstackRouter.test.ts
Code ReviewOverviewThis PR fixes a React hydration mismatch in The key insight is also preserved: the route-chunk preload behavior is not lost — ✅ What the PR gets right
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 77b1845944
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
Code Review - see inline comments for details. Overall verdict: Approve. The Suspense removal is the correct fix for the hydration mismatch. Chunk-preload sequencing is preserved via routeChunkPreloadPromiseRef awaited in runPostHydrationLoad. Two focused regression tests cover both behaviors. One concern worth a comment: RouteChunkPreloadGate also protected subsequent re-renders (after hydration, before router.load()) from rendering while chunks are in-flight. That protection is now gone. The router.ssr flag mitigates this for navigation, but a parent-triggered re-render in that window could reach RouterProvider before chunks settle. The window is short and risk is low, but worth documenting inline so a future developer does not re-introduce the gate for the wrong reason. |
Code ReviewOverviewThis PR fixes a real and significant bug: What works well
Points to addressSubtle behavior change worth documenting — With
Lychee / CHANGELOG changesThe two new SummaryThe fix is correct, the dead-code removal is complete, and the regression tests cover both the ordering invariant and the no-Suspense structural guarantee. The points above are minor. Approving with a suggestion to add a short note about the re-render behavior change alongside the existing comment block near the |
d56f71f to
87262a6
Compare
Code ReviewSummaryThis is a well-targeted fix for a genuine React hydration mismatch bug. The root cause analysis is correct: the server emits The chunk-preload sequencing is preserved correctly by awaiting Issues1. Non-standard use of
|
Code Review — PR #3213SummaryCorrect and well-motivated fix. The root cause (Suspense shape mismatch between server and client renders) is accurately diagnosed, and rendering What's good
Observations (non-blocking)1. Import style — The hybrid 2. React scheduler assumption — The deferred 3. VerdictApprove. The fix is correct, tested, and well-documented. Observations are cosmetic/informational and should not block merge. |
|
Address-review checkpoint for feedback after Mattered:
Optional:
Skipped:
Validation:
Future full-PR scans should start after this comment unless |
|
Address-review checkpoint for feedback after Mattered:
Optional:
Skipped:
Validation:
Additional cleanup after the first checkpoint:
No additional code changes were made after Future full-PR scans should start after this comment unless |
* origin/main: (54 commits) Bump version to 16.7.0.rc.1 [codex] Prepare 16.7.0.rc.1 changelog (#3384) fix(tanstack-router): remove Suspense gate around RouterProvider during hydration (#3213) chore(deps): bump the npm-security group across 1 directory with 3 updates (#3370) fix: add reactDomClientWarning helper for React 16/17 (#3137) (#3358) docs: document RSC node renderer globals (#3221) fix(pro): preserve react-on-rails-rsc/client.browser in client bundle (#3366) (#3368) Allow Pro 16.7 to keep jwt 2.x compatibility (#3344) Plan: flagship 2026 React on Rails starter kit (#3357) (#3364) docs: clarify coupled gem/npm/lockfile upgrade for React on Rails Pro (#3369) chore(deps): bump fastify from 5.8.3 to 5.8.5 in the npm-security group across 1 directory (#3152) docs: clarify initial security triage ownership in SECURITY.md (#3337) docs: consolidate node renderer probe settings (#3257) (#3348) chore(ci): remove obsolete setup-node-with-retry action (#3352) chore: remove completed planning/analysis docs (#3361) test: stub stable VERSION in --pro install spec to unblock CI (#3363) docs(specs): add RC testing plan design spec (#3350) docs: point users at bin/shakapacker-config --doctor for bundler-config debugging (#3359) docs: polish RSC node renderer test recipe (#3273) (#3339) test: replace call_count stub pattern with and_return sequence (#2157) (#3353) ...
* origin/main: (54 commits) Bump version to 16.7.0.rc.1 [codex] Prepare 16.7.0.rc.1 changelog (#3384) fix(tanstack-router): remove Suspense gate around RouterProvider during hydration (#3213) chore(deps): bump the npm-security group across 1 directory with 3 updates (#3370) fix: add reactDomClientWarning helper for React 16/17 (#3137) (#3358) docs: document RSC node renderer globals (#3221) fix(pro): preserve react-on-rails-rsc/client.browser in client bundle (#3366) (#3368) Allow Pro 16.7 to keep jwt 2.x compatibility (#3344) Plan: flagship 2026 React on Rails starter kit (#3357) (#3364) docs: clarify coupled gem/npm/lockfile upgrade for React on Rails Pro (#3369) chore(deps): bump fastify from 5.8.3 to 5.8.5 in the npm-security group across 1 directory (#3152) docs: clarify initial security triage ownership in SECURITY.md (#3337) docs: consolidate node renderer probe settings (#3257) (#3348) chore(ci): remove obsolete setup-node-with-retry action (#3352) chore: remove completed planning/analysis docs (#3361) test: stub stable VERSION in --pro install spec to unblock CI (#3363) docs(specs): add RC testing plan design spec (#3350) docs: point users at bin/shakapacker-config --doctor for bundler-config debugging (#3359) docs: polish RSC node renderer test recipe (#3273) (#3339) test: replace call_count stub pattern with and_return sequence (#2157) (#3353) ... # Conflicts: # docs/oss/core-concepts/performance-benchmarks.md
* origin/main: Bump version to 16.7.0.rc.1 [codex] Prepare 16.7.0.rc.1 changelog (#3384) fix(tanstack-router): remove Suspense gate around RouterProvider during hydration (#3213) chore(deps): bump the npm-security group across 1 directory with 3 updates (#3370) fix: add reactDomClientWarning helper for React 16/17 (#3137) (#3358) docs: document RSC node renderer globals (#3221) fix(pro): preserve react-on-rails-rsc/client.browser in client bundle (#3366) (#3368) Allow Pro 16.7 to keep jwt 2.x compatibility (#3344) Plan: flagship 2026 React on Rails starter kit (#3357) (#3364) docs: clarify coupled gem/npm/lockfile upgrade for React on Rails Pro (#3369) chore(deps): bump fastify from 5.8.3 to 5.8.5 in the npm-security group across 1 directory (#3152) docs: clarify initial security triage ownership in SECURITY.md (#3337) docs: consolidate node renderer probe settings (#3257) (#3348) chore(ci): remove obsolete setup-node-with-retry action (#3352) chore: remove completed planning/analysis docs (#3361)
* origin/main: (26 commits) docs: define security-supported versions policy (#3261) (#3341) Bump version to 16.7.0.rc.2 Update CHANGELOG.md for 16.7.0.rc.2 (#3393) Fail release on unverifiable or Yarn-incompatible npm packages (#3387) Bump version to 16.7.0.rc.1 [codex] Prepare 16.7.0.rc.1 changelog (#3384) fix(tanstack-router): remove Suspense gate around RouterProvider during hydration (#3213) chore(deps): bump the npm-security group across 1 directory with 3 updates (#3370) fix: add reactDomClientWarning helper for React 16/17 (#3137) (#3358) docs: document RSC node renderer globals (#3221) fix(pro): preserve react-on-rails-rsc/client.browser in client bundle (#3366) (#3368) Allow Pro 16.7 to keep jwt 2.x compatibility (#3344) Plan: flagship 2026 React on Rails starter kit (#3357) (#3364) docs: clarify coupled gem/npm/lockfile upgrade for React on Rails Pro (#3369) chore(deps): bump fastify from 5.8.3 to 5.8.5 in the npm-security group across 1 directory (#3152) docs: clarify initial security triage ownership in SECURITY.md (#3337) docs: consolidate node renderer probe settings (#3257) (#3348) chore(ci): remove obsolete setup-node-with-retry action (#3352) chore: remove completed planning/analysis docs (#3361) test: stub stable VERSION in --pro install spec to unblock CI (#3363) ...
* origin/main: (31 commits) More precise heading in CHANGELOG (#3402) Update `pino` dependency range (#3401) Upgrade to latest pnpm v10 (#3400) docs: track CI_PNPM_FALLBACK_VERSION bumps in CONTRIBUTING (#3264) (#3346) docs(generator): record pnpm fallback bump policy and last-checked date (#3248) (#3328) docs: define security-supported versions policy (#3261) (#3341) Bump version to 16.7.0.rc.2 Update CHANGELOG.md for 16.7.0.rc.2 (#3393) Fail release on unverifiable or Yarn-incompatible npm packages (#3387) Bump version to 16.7.0.rc.1 [codex] Prepare 16.7.0.rc.1 changelog (#3384) fix(tanstack-router): remove Suspense gate around RouterProvider during hydration (#3213) chore(deps): bump the npm-security group across 1 directory with 3 updates (#3370) fix: add reactDomClientWarning helper for React 16/17 (#3137) (#3358) docs: document RSC node renderer globals (#3221) fix(pro): preserve react-on-rails-rsc/client.browser in client bundle (#3366) (#3368) Allow Pro 16.7 to keep jwt 2.x compatibility (#3344) Plan: flagship 2026 React on Rails starter kit (#3357) (#3364) docs: clarify coupled gem/npm/lockfile upgrade for React on Rails Pro (#3369) chore(deps): bump fastify from 5.8.3 to 5.8.5 in the npm-security group across 1 directory (#3152) ... # Conflicts: # CHANGELOG.md
…#3405) (#3410) ## Summary - Fixes the `tanstack-router integration (Pro) › does not double-call loadRouteChunk when StrictMode replays hydration effects` test that has been failing on every push to `main` since [PR #3213](#3213). - Adds a module-level `WeakMap<TanStackRouter, SharedHydrationInitState>` in `clientHydrate.ts` that memoizes the render-phase init per router instance, dedup'ing `loadRouteChunk`, `__store.setState`, and the user-defined `hydrate` callback across React 18 StrictMode's double-render-with-fresh-hooks behavior. - Production is unaffected: each mount calls `options.createRouter()` and gets a fresh router, so the `WeakMap` always misses and the existing init runs. Fixes #3405. ## Root cause React 18's StrictMode renders components twice in dev with **separate hook state per pass** (verified empirically against `react@18.3.1`): ``` React 18: React 19: Render 1, ref.current === null: true Render 1, ref.current === null: true Render 2, ref.current === null: true Render 2, ref.current === null: false refSetCount: 2 refSetCount: 1 ``` The `routerRef.current === null` guard relied on React 19's behavior of preserving `useRef` across the two passes, so on React 18 it fired twice when the user (or test) returned the same router from `options.createRouter`. The previous "Safety invariant" comment at `clientHydrate.ts:186-196` accounted for **discarded renders**, where the discarded router instance is dropped. That's still correct — but it didn't cover the case where the same router instance survives across passes (cached by the user's factory) and the discarded render's side effects on it leak. ## Fix `sharedHydrationInitStates: WeakMap<router, {routeChunkPreloadPromise, hydrationCallbackPromise, didSetSsrFlag}>` is checked at the top of the init block. On a cache hit: - Reattach the cached promises to this mount's `routeChunkPreloadPromiseRef` / `hydrationCallbackPromiseRef` so the post-hydration effect still awaits the original work before calling `router.load()` (otherwise `router.load` would race the in-flight preload). - Restore `didSetSsrFlagRef` so the cleanup path correctly clears `router.ssr`. - Skip `router.update({ history })`, `__store.setState`, `router.ssr` writes, and the user-defined `hydrate` callback. Per-router-instance memoization means: - **Production:** every mount creates a new router → WeakMap miss → full init. Identical to today. - **Tests / cached routers / StrictMode:** same router across mounts → WeakMap hit → init runs exactly once. ## Test plan - [x] Reproduced the bug on React 18.0.0 locally via `script/convert` + `pnpm install --no-frozen-lockfile`, confirmed `Expected: 1, Received: 2` before the fix. - [x] All 29 `tanstackRouter.test.ts` tests pass on **React 18.0.0** (the minimum-supported Node 20 CI matrix). - [x] All 70 Pro JS tests (63 non-RSC + 7 RSC) pass on **React 19.2.6** (default). - [x] `pnpm --filter react-on-rails-pro type-check` clean. - [x] `pnpm lint` clean. - [x] No changes outside `packages/react-on-rails-pro/src/tanstack-router/clientHydrate.ts` and `CHANGELOG.md`. ## What's still in scope to watch - The existing `initializes hydration store injection once per created router in StrictMode` test creates a **new router per `createRouter` call** — that's still a WeakMap miss for each, so each router's `setState` fires once. Verified passing on both React 18 and 19. - The `waits for the matched route chunk preload promise before triggering post-hydration router.load` test (single mount, no StrictMode) still fires the preload exactly once. Verified. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Scoped to Pro client hydration in dev/StrictMode and cached-router scenarios; production paths with fresh routers per mount are unchanged. > > **Overview** > Fixes **Pro TanStack Router client hydration** under **React 18 StrictMode** when `createRouter` returns a **reused router instance** (common in tests or user caching). > > Render-phase hydration in `clientHydrate.ts` still runs only when `routerRef.current === null`, but StrictMode resets refs between passes, so that guard could run twice and repeat **`loadRouteChunk`**, **`__store.setState`**, and **`options.hydrate`**. A module-level **`WeakMap`** keyed by the router instance now runs that init once per router; cache hits only reattach preload/hydrate promises and **`didSetSsrFlag`** for the post-hydration effect and cleanup. **`router.ssr`** clearing updates the WeakMap so a full remount of the same router does not trigger a false dev warning. > > Production behavior is unchanged when each mount gets a new router. **CHANGELOG** documents the fix for issue #3405; a regression test covers remount of a cached router. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 67a22f8. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Fixed TanStack Router hydration under React 18 StrictMode to stop duplicate data loads and repeated user hydrate callbacks [Pro]. * Prevented spurious developer warnings when fully unmounting and remounting the same router instance in dev StrictMode. * **Tests** * Added a regression test ensuring unmount/remount of the same router instance does not emit the dev warning. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/shakacode/react_on_rails/pull/3410?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
Summary
TanStackHydrationAppwas wrappingRouterProviderinSuspense+RouteChunkPreloadGate, butserverRender.ts'sbuildAppElementemitsAppWrapper > RouterProviderdirectly with no Suspense boundary. The extra client-side boundary produces a Suspense-shape mismatch during hydration and React bails to a full client-side re-render — defeating SSR.This change renders
RouterProviderdirectly so the client tree mirrors the server output exactly.What changed
packages/react-on-rails-pro/src/tanstack-router/clientHydrate.tscreateElement(RouterProvider, { router })directly (noSuspense/gate wrapper).Suspenseimport,RouteChunkPreloadGatecomponent,RouteChunkPreloadGatePropsinterface, androuteChunkPreloadSettledRef(only the gate read it).routeChunkPreloadPromiseRefis preserved —runPostHydrationLoadstill awaits it beforerouter.load(), so chunk-preload behavior for post-hydration navigation is unchanged.Test plan
packages/react-on-rails-pro/tests/tanstackRouter.test.tspass<Link>navigation still works after hydration (verifies post-mountrouter.load()still runs)Origin
Originally landed in
printivity/printivityas a yarn patch on the publishedreact-on-rails-pro16.6.0 npm package (see.yarn/patches/react-on-rails-pro-npm-16.6.0-183d3c1ef3.patchin #2843). Porting it upstream so the patch can be removed.https://claude.ai/code/session_01NzWXywhF3M8h8zU8n8u3qX
Generated by Claude Code
Summary by CodeRabbit