Skip to main content
Version: 17.x (stable)

Using TanStack Router

TanStack Router is a fully type-safe React router with built-in data loading, search param validation, and first-class TypeScript support. It's a strong choice when you want end-to-end type safety from your route definitions through to your components and search params.

React on Rails Pro supports TanStack Router SSR via react-on-rails-pro/tanstack-router. This helper creates a render-function that handles both server-side rendering and client hydration/navigation.

TanStack Router SSR requires React on Rails Pro with the Node Renderer and rendering_returns_promises = true in your Pro configuration. The async Node Renderer uses TanStack Router's public router.load() API for reliable, maintainable SSR.

Client-side-only TanStack Router (without SSR) works with the open-source package - no special integration is needed since it's just a standard React app.

Support Model​

  • First-class TanStack Router SSR: React on Rails Pro only, via react-on-rails-pro/tanstack-router
  • Server renderer: Pro Node Renderer with rendering_returns_promises = true
  • ExecJS: intentionally not supported for TanStack Router SSR
  • Client-only TanStack Router: works in OSS without any special helper
  • React Router: remains the manual integration option; see Using React Router

Why Pro-Only​

This guide intentionally avoids an ExecJS SSR path.

Synchronous SSR with TanStack Router pushes React on Rails toward private TanStack Router internals. The Pro-only async path keeps the integration on TanStack Router's public router.load() API and the dehydrate/hydrate router options, which is a much better long-term maintenance boundary.

Install​

pnpm add @tanstack/react-router

Register a TanStack Router App​

Create a render-function with createTanStackRouterRenderFunction and register it with React on Rails:

import ReactOnRails from 'react-on-rails-pro';
import { createTanStackRouterRenderFunction } from 'react-on-rails-pro/tanstack-router';
import {
RouterProvider,
createBrowserHistory,
createMemoryHistory,
createRouter,
} from '@tanstack/react-router';
import { routeTree } from '../routes/routeTree.gen';

const TanStackApp = createTanStackRouterRenderFunction(
{
createRouter: () => createRouter({ routeTree }),
// AppWrapper: ({ children }) => <MyProviders>{children}</MyProviders>, // optional
},
{
RouterProvider,
createMemoryHistory,
createBrowserHistory,
},
);

ReactOnRails.register({ TanStackApp });

Then render it from Rails:

<%= react_component("TanStackApp", props: { currentUserId: current_user&.id }, prerender: true) %>

How SSR + Hydration Works​

On the server, the helper returns a server render hash:

{
renderedHtml: ReactElement,
clientProps: {
__tanstackRouterDehydratedState: { ... }
}
}

React on Rails then:

  1. Renders renderedHtml into HTML.
  2. Merges clientProps into the client hydration payload generated by react_component.
  3. Hydrates TanStack Router on the client using the dehydrated router state.

Ship External State with dehydrate and hydrate​

To send other state with the server render, such as a TanStack Query cache, pass dehydrate and hydrate to createRouter. The helper calls dehydrate() once on the server after router.load() finishes, and awaits it if it returns a promise. On the client, it calls hydrate() with that value during the first render. Fill caches synchronously in hydrate: the helper waits for a returned promise only before the first client-side router.load(), not before the first render, so data an async hydrate adds later can cause a hydration mismatch.

import { QueryClient, dehydrate, hydrate } from '@tanstack/react-query';

// Inside the createRouter function you pass to createTanStackRouterRenderFunction:
const queryClient = new QueryClient();
const router = createRouter({
routeTree,
context: { queryClient },
dehydrate: () => ({ queryClientState: dehydrate(queryClient) }),
hydrate: (dehydrated) => hydrate(queryClient, dehydrated.queryClientState),
});

Create a new QueryClient in each createRouter call so server renders never share a cache.

The dehydrated value reaches the client as JSON inside the react_component props. Values JSON cannot represent lose their type on the way: a Date arrives as a string, and an Error or Map arrives as a plain object. Convert such values to JSON-safe data in dehydrate and rebuild them in hydrate.

Loader Data and Route Errors​

Loader data and beforeLoad context travel through JSON and must be JSON-safe. Return strings instead of Date instances (for example, date.toISOString()), arrays instead of Set, and plain objects instead of Map. Convert values before rendering on the server so the first client render sees the same types. In development, the helper warns when loader data contains non-JSON values; it does not convert them.

A route match's thrown Error is restored as an Error before the first client render. Outside explicit development (NODE_ENV=development), the server replaces it with Error: An unexpected error occurred. before rendering and dehydration. The browser restores the server's chosen fields during hydration, even when bundle modes differ. Original names, messages, stacks, and custom properties are omitted from the production error payload. Capture private details on the server before error rendering if your application needs them for diagnostics.

Development preserves the original name, message, and stack. Custom error subclass identity, additional properties, and cause are not transported. Plain thrown objects, including TanStack Router's not-found objects, retain their existing JSON representation; applications must keep those objects suitable for clients.

Props Requirement​

When using this helper with prerender: true, pass props: as a Ruby Hash or a JSON object string. If you pass a JSON string that parses to a non-object value (like an array), React on Rails raises an error.

Pro Configuration​

Ensure your React on Rails Pro initializer includes:

ReactOnRailsPro.configure do |config|
# TanStack Router SSR requires the Pro Node Renderer async path.
config.rendering_returns_promises = true
end

Compatibility​

  • Supported @tanstack/react-router versions: >=1.139.0 <2.0.0
  • Keep react-on-rails-pro up to date when upgrading @tanstack/react-router.

Working Examples in This Repo​

  • Pro dummy app (async server rendering):
    • Route + view:
      • react_on_rails_pro/spec/dummy/config/routes.rb
      • react_on_rails_pro/spec/dummy/app/views/tanstack_router/index.html.erb
    • TanStack app entry:
      • react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/TanStackRouterAppAsync.jsx
    • System test:
      • react_on_rails_pro/spec/dummy/spec/system/integration_spec.rb

References​