Add TanStack Router SSR integration utility#2516
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:
WalkthroughAdds TanStack Router integration and wiring: package export and optional peer dependency; TypeScript types; server sync/async render helpers; client hydration utility; threading of server-returned clientProps into Rails helper merge; docs, tests, demo apps, and example routes. Changes
Sequence Diagram(s)sequenceDiagram
participant Controller as Rails Controller
participant Server as SSR Process
participant Router as TanStack Router
participant Store as Router.__store
participant Renderer as React Renderer
Controller->>Server: call renderFunction(props with url, railsContext.serverSide=true)
Server->>Router: createRouter(createMemoryHistory(initialEntries=[url]))
Server->>Router: validate internals / matchRoutes(path)
Server->>Store: inject matches sync (__store setState)
Server->>Router: router.ssr = true
Server->>Router: dehydrated = router.dehydrate()
Server->>Renderer: build appElement (RouterProvider + AppWrapper)
Renderer-->>Server: appElement
Server-->>Controller: return { renderedHtml: appElement, clientProps: { __tanstackRouterDehydratedState } }
sequenceDiagram
participant Browser as Client Browser
participant Hydrator as clientHydrateTanStackApp
participant History as createBrowserHistory
participant Router as TanStack Router
participant Effect as post-hydration effect
Browser->>Hydrator: init(props may include dehydratedState)
Hydrator->>History: createBrowserHistory()
Hydrator->>Router: createRouter(browserHistory)
alt dehydratedState present
Hydrator->>Router: router.hydrate(dehydratedState)
Hydrator->>Router: router.ssr = true
else no dehydratedRouter
Hydrator->>Router: router.matchRoutes(path) -> inject matches into __store
end
Browser->>Effect: useEffect -> if dehydratedState then router.load()
Router-->>Browser: RouterProvider renders hydrated app
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 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 |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is ON, but it could not run because Privacy Mode (Legacy) is turned on. To enable Bugbot Autofix, switch your privacy mode in the Cursor dashboard.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5226885a84
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Greptile SummaryThis PR adds a new The implementation is well-structured with clear comments explaining private-API workarounds. However, three issues need addressing:
Confidence Score: 2/5
Sequence DiagramsequenceDiagram
participant RoR as React on Rails
participant RenderFn as createTanStackRouterRenderFunction
participant Server as serverRenderTanStackApp
participant Client as clientHydrateTanStackApp
participant Router as TanStack Router
Note over RoR,Router: Server-Side Rendering (sync)
RoR->>RenderFn: renderFn(props, railsContext{serverSide:true})
RenderFn->>Server: serverRenderTanStackApp(options, props, railsContext)
Server->>Router: options.createRouter()
Server->>Router: router.update({history: memoryHistory})
Server->>Router: validateRouterInternals(router)
Server->>Router: router.matchRoutes(pathname, search)
Server->>Router: router.__store.setState({status:'idle', matches})
Server->>Router: router.ssr = true
Server->>Router: router.dehydrate()
Server-->>RenderFn: ReactElement (with __tanstackRouterDehydratedState in props)
RenderFn-->>RoR: ReactElement → renderToString()
Note over RoR,Router: Client-Side Hydration
RoR->>RenderFn: renderFn(props, railsContext{serverSide:false})
RenderFn->>Client: clientHydrateTanStackApp(options, props, railsContext)
Client->>Router: options.createRouter()
Client->>Router: router.update({history: browserHistory})
Client->>Router: router.__store.setState({status:'idle', matches})
Client->>Router: router.ssr = true
Client->>Router: router.hydrate(dehydratedState.dehydratedRouter)
Client-->>RenderFn: ReactElement (AppComponent)
RenderFn-->>RoR: ReactElement → hydrateRoot()
Note over Client,Router: useEffect (post-mount): router.load() [currently unreachable]
Last reviewed commit: 5226885 |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
packages/react-on-rails/src/tanstack-router/types.ts (1)
11-21: Widen search value typing to avoid rejecting valid typed routers.Lines [13] and [20] use
Record<string, string>, which is stricter than many TanStack Router search schemas (often non-string values). This can makecreateRouterreturn types harder to assign.🔧 Proposed change
matchRoutes: ( pathname: string, - locationSearch: Record<string, string>, + locationSearch: Record<string, unknown>, opts?: { throwOnError?: boolean }, ) => unknown[]; @@ location: { pathname: string; - search: Record<string, string>; + search: Record<string, unknown>; searchStr: string; hash: string; href: string; };Also applies to: 25-27
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/react-on-rails/src/tanstack-router/types.ts` around lines 11 - 21, The type for search is too narrow (Record<string, string>) and rejects valid TanStack Router schemas; update the search typing used in matchRoutes signature and in state.location (and the other occurrences around the same area) to a wider type such as Record<string, unknown> (or Record<string, any>) so non-string search values are accepted; locate the matchRoutes declaration and the state { location { search } } type blocks and replace the Record<string, string> occurrences with the wider type.
🤖 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-on-rails/package.json`:
- Around line 62-67: The peer dependency range for "@tanstack/react-router" is
too loose (">= 1.0.0") and can pull in breaking v2+ changes; update the
peerDependencies entry for "@tanstack/react-router" to constrain to v1.x (for
example ">=1.0.0 <2.0.0") so consumers cannot install incompatible v2+ releases
that break uses of internal APIs like router.__store.setState and router.ssr.
In `@packages/react-on-rails/src/tanstack-router/clientHydrate.ts`:
- Around line 44-47: The initialization forcibly setting status to 'idle'
prevents the later condition that calls router.load() from ever running; remove
the status override so the initial state uses the router's real status and then
unconditionally call router.load() on first client render (respecting router.ssr
= true behavior) to enable client-side navigation — update the clientHydrate
initialization that assigns status and resolvedLocation and ensure router.load()
is invoked (call router.load() directly or remove the status check gating it) so
that Router.load() executes after the first render.
In `@packages/react-on-rails/src/tanstack-router/index.ts`:
- Line 39: The public entrypoint currently only re-exports types
(TanStackRouterOptions, DehydratedRouterState) and the sync factory but omits
the async SSR helper; add an explicit export for serverRenderTanStackAppAsync so
consumers can import it from the package surface (e.g. export {
serverRenderTanStackAppAsync } from './serverRenderTanStackAppAsync' or from the
module where it's implemented) and similarly ensure the same export is added in
the other export block referenced (lines ~106-117) so the async helper is
available from both public entrypoints; reference the symbol
serverRenderTanStackAppAsync when adding the export.
---
Nitpick comments:
In `@packages/react-on-rails/src/tanstack-router/types.ts`:
- Around line 11-21: The type for search is too narrow (Record<string, string>)
and rejects valid TanStack Router schemas; update the search typing used in
matchRoutes signature and in state.location (and the other occurrences around
the same area) to a wider type such as Record<string, unknown> (or
Record<string, any>) so non-string search values are accepted; locate the
matchRoutes declaration and the state { location { search } } type blocks and
replace the Record<string, string> occurrences with the wider type.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 60276abb-c2aa-4f10-860f-3231180d4b00
📒 Files selected for processing (5)
packages/react-on-rails/package.jsonpackages/react-on-rails/src/tanstack-router/clientHydrate.tspackages/react-on-rails/src/tanstack-router/index.tspackages/react-on-rails/src/tanstack-router/serverRender.tspackages/react-on-rails/src/tanstack-router/types.ts
size-limit report 📦
|
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/api-reference/view-helpers-api.md`:
- Line 27: Update the wording for the renderer-function sentence in the
component_name description: change "dom (client side only)" to "DOM (client-side
only)" and hyphenate "renderer function" consistently where applicable; ensure
the phrase still clarifies that a "renderer function" takes a third parameter of
a DOM ID and that it's client-side only, referencing the symbol component_name
and the term "renderer function"/"Render-Function".
In `@docs/building-features/tanstack-router.md`:
- Around line 13-15: Update the installation code block in the tanstack-router
docs to be package-manager-agnostic by adding npm and yarn variants alongside
the existing pnpm command; specifically replace the single-line pnpm install
snippet with three lines showing "npm install `@tanstack/react-router`", "yarn add
`@tanstack/react-router`", and "pnpm add `@tanstack/react-router`" in the same
fenced bash block so readers can use their preferred package manager.
In `@packages/react-on-rails/src/tanstack-router/serverRender.ts`:
- Around line 11-27: validateRouterInternals currently only checks
__store.setState and matchRoutes but not the shape of router.state.location,
which later leads to unsafe dereferences of router.state.location.pathname and
.search; update validateRouterInternals to also verify router.state and
router.state.location exist and that location.pathname and location.search are
strings (or at least defined), and throw a clear compatibility Error (same style
as existing messages) mentioning validateRouterInternals and
router.state.location so callers get a helpful message if `@tanstack/react-router`
changes its location shape.
In `@react_on_rails/lib/react_on_rails/helper.rb`:
- Around line 583-588: Reorder and tighten the checks around client_props: first
handle nil (return if client_props.nil?), then validate its type (unless
client_props.is_a?(Hash) raise ReactOnRails::Error ...), and only after
confirming it's a Hash call client_props.empty? and return if empty; this
ensures client_props.empty? is never invoked on non-Hash scalars—refer to the
local variable client_props and the raised ReactOnRails::Error for where to
apply the change.
- Around line 590-596: The code in the react_on_rails helper incorrectly raises
ReactOnRails::Error when render_options.props is a JSON string but clientProps
exists; instead, update the merge logic in the block referencing existing_props
and render_options.props to accept a JSON-string props by parsing the JSON
string into a Hash (or safely converting it) before merging clientProps,
handling JSON parse errors by raising the current error only if parsing fails;
ensure the same merge path is used whether props started as a Hash or a JSON
string so react_component’s documented JSON-string support remains compatible.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 66ced15e-de29-4f26-af36-0cfa3c4b4d01
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (19)
docs/README.mddocs/api-reference/javascript-api.mddocs/api-reference/view-helpers-api.mddocs/building-features/react-router.mddocs/building-features/tanstack-router.mddocs/core-concepts/render-functions.mddocs/getting-started/using-react-on-rails.mddocs/introduction.mdpackages/react-on-rails/src/serverRenderReactComponent.tspackages/react-on-rails/src/serverRenderUtils.tspackages/react-on-rails/src/tanstack-router/clientHydrate.tspackages/react-on-rails/src/tanstack-router/index.tspackages/react-on-rails/src/tanstack-router/serverRender.tspackages/react-on-rails/src/tanstack-router/types.tspackages/react-on-rails/src/types/index.tspackages/react-on-rails/tests/serverRenderReactComponent.test.tspackages/react-on-rails/tests/tanstackRouter.test.tsreact_on_rails/lib/react_on_rails/helper.rbreact_on_rails/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/react-on-rails/src/tanstack-router/clientHydrate.ts
| "Pass props as a Hash, not #{class_name}." | ||
| end | ||
|
|
||
| render_options.set_option(:props, merge_client_props(existing_props, client_props)) |
There was a problem hiding this comment.
client_props keys are trusted data from the Node renderer — clarify the trust boundary
client_props values flow from the JS server render result directly into the JSON hydration payload sent to the client. If client_props contains a key like authenticity_token or current_user, it would override the matching prop silently.
Consider adding a safelist of allowed keys (or at minimum a denylist of reserved keys), or at least call out the trust model in a comment so future maintainers know not to tighten / loosen it without understanding the security implications:
# client_props originates from the JS server renderer output. It is treated as
# trusted server-controlled data; do not pass user-controlled values here.|
Review summary — all details are in the inline comments above. Bugs: (1) clientHydrate.ts line 121: router.ssr !== true should be !router.ssr to handle the object-valued case without dropping manifest data. (2) clientHydrate.ts line 127: setTanStackSsrGlobal is a DOM write in the render body — move to useLayoutEffect to be safe under React 18 Strict Mode double-invoke. API clarity: TanStackRouterAppAsync.jsx manually re-implements the server branch of createTanStackRouterRenderFunction. The example should just be const TanStackRouterAppAsync = createTanStackRouterRenderFunction(options, deps). Fragility: dehydrateSsrMatchId mirrors a TanStack Router private wire format. Document the upstream file and version range. The write-guard in enableRouterSsrMode is unreliable in non-strict-mode JS; simplify to just the assignment with a comment. Nits: ignoreDependencies: [] in knip.ts is a no-op. A comment on merge_server_rendered_client_props! clarifying client_props is server-controlled (not user-input) would help future maintainers. What is solid: isServerRenderHash refactor, async promise dispatch to processServerRenderHash, Ruby merge_client_props key normalization with clear errors, and strong edge-case test coverage. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 86da107bac
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (hasSsrRouter) { | ||
| // RouterClient will hydrate the route matches from window.$_TSR. | ||
| } else if (hasDehydratedRouter && typeof router.hydrate === 'function') { | ||
| router.hydrate(dehydratedState.dehydratedRouter); |
There was a problem hiding this comment.
Hydrate router when RouterClient is absent
serverRenderTanStackAppAsync always sends an ssrRouter payload, so this if (hasSsrRouter) branch runs on every SSR hydration and skips router.hydrate(...). In the documented/default setup where RouterClient is not provided, the code later renders RouterProvider, which does not consume window.$_TSR, so the dehydrated server state is never applied; initial route loader state is lost and clients can re-fetch on mount (or render from an unhydrated router state), causing hydration inconsistencies and duplicate data loads.
Useful? React with 👍 / 👎.
RouterClient wraps RouterProvider in <Await> which always suspends on first render (defer() starts with status 'pending', resolves on next microtask). Since the server renders with RouterProvider directly (no <Await> wrapper), the structural difference causes React hydration mismatch errors: "Hydration failed because the initial UI does not match what was rendered on the server." React recovers by discarding the server HTML and re-rendering on the client, but this defeats the performance benefit of SSR. Fix: use RouterProvider directly on the client (matching the server tree) and synchronously inject route matches via __store.setState() + router.ssr flag — the same approach used by the open-source react-on-rails tanstack-router integration (commit 15410e2). The RouterClient parameter is kept in the function signature for backward compatibility but is intentionally unused. Ref: shakacode#2516 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Summary
Adds first-class TanStack Router SSR support for React on Rails Pro through
react-on-rails-pro/tanstack-router.What Changed
createTanStackRouterRenderFunction()andserverRenderTanStackAppAsync()toreact-on-rails-pro/tanstack-router.rendering_returns_promises = true) and TanStack Router's publicrouter.load()API.RouterClientand TanStack Router's$_TSRSSR payload, preserves client-only initial-load behavior, and keeps the internal hydration payload out ofAppWrapperprops.renderedHtmlas a React element plusclientProps, including correct async server-render-hash handling.react_componenthelper to merge server-renderedclientPropsinto the client hydration payload.Scope And Compatibility
rendering_returns_promises = true.@tanstack/react-routerversions:>=1.139.0 <2.0.0.Coverage
clientPropsmerging.Closes #2298