docs/oss/building-features/react-router.md
React on Rails supports React Router for client-side routing. This guide shows how to integrate React Router into your React on Rails application.
Looking for TanStack Router? See Using TanStack Router.
Important: The React on Rails generator does not install React Router. You'll need to add it to your project manually.
React Router support in React on Rails is a manual integration pattern. If you want a first-class SSR helper with router dehydration/hydration, use TanStack Router with React on Rails Pro.
If you're using Turbo (Rails 7+) or Turbolinks (Rails 6 and earlier), be aware that both React Router and Turbo/Turbolinks handle browser navigation and the back button. These two routing systems can conflict. Consider:
data-turbo="false"If you successfully integrate both, please share your solution with the community!
For more details, see Turbo/Turbolinks Guide.
First, add React Router v6 to your project:
npm install react-router-dom@^6.0.0
# or: yarn add react-router-dom@^6.0.0
# or: pnpm add react-router-dom@^6.0.0
Why React Router v6? React Router v7 has merged with Remix and uses a different architecture that may not be fully compatible with React on Rails' manual server-side rendering approach. We recommend v6 for stable integration. If you need v7 features, please test thoroughly and share your findings with the community.
React Router v6 offers multiple routing approaches. For React on Rails, we recommend Declarative Mode (traditional component-based routing, covered in this guide).
Note on Data Mode: React Router's Data Mode (with loaders/actions) is designed for SPAs where the client handles data fetching. Since React on Rails uses Rails controllers to load data and pass it as props to React components, Data Mode would create duplicate data loading. Stick with Declarative Mode to leverage React on Rails' server-side data loading pattern.
Most React Router integrations do not need Redux. Route ordinary components inside one React root, pass initial data from Rails as props, and use your normal server-state approach for follow-up data loading.
File: app/javascript/src/RouterApp/RouterRoutes.jsx
import React from 'react';
import { Routes, Route } from 'react-router-dom';
const Home = ({ name }) => <div>Hello, {name}!</div>;
const About = () => <div>About</div>;
const RouterRoutes = (props) => (
<Routes>
<Route path="/hello_world" element={<Home {...props} />} />
<Route path="/hello_world/about" element={<About />} />
</Routes>
);
export default RouterRoutes;
RouterRoutes.jsx is a shared route tree that expects a router context from its parent. Keep it outside
ror_components/ and do not register it directly with React on Rails. Only the files inside
ror_components/ are registered entry points.
File: app/javascript/src/RouterApp/ror_components/RouterApp.client.jsx
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import RouterRoutes from '../RouterRoutes';
const RouterApp = (props) => (
<BrowserRouter>
<RouterRoutes {...props} />
</BrowserRouter>
);
export default RouterApp;
If you're maintaining an app that already uses Redux, including the hidden legacy Redux generator output, you can add React Router by wrapping your app:
File: app/javascript/src/HelloWorldApp/ror_components/HelloWorldApp.client.jsx
import React from 'react';
import { Provider } from 'react-redux';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import configureStore from '../store/helloWorldStore';
import HelloWorldContainer from '../containers/HelloWorldContainer';
// Import other components for routing
// import About from '../components/About';
// import Contact from '../components/Contact';
const HelloWorldApp = (props) => {
const store = configureStore(props);
return (
<Provider store={store}>
<BrowserRouter>
<Routes>
<Route path="/" element={<HelloWorldContainer />} />
</Routes>
</BrowserRouter>
</Provider>
);
};
export default HelloWorldApp;
Key points:
<Provider> wraps <BrowserRouter> so all routes have Redux access<Routes> and <Route> (not <Switch> from React Router v5)element prop to specify components (not component or render props from v5)For server rendering without Redux, use the same route tree with StaticRouter instead of BrowserRouter.
This .client.jsx / .server.jsx split assumes
auto-bundling
is configured so React on Rails registers the client and server files separately.
The server entry returns a render function, not JSX directly; see
Render-Functions and railsContext.
File: app/javascript/src/RouterApp/ror_components/RouterApp.server.jsx
import React from 'react';
import { StaticRouter } from 'react-router-dom/server';
import RouterRoutes from '../RouterRoutes';
const RouterApp = (props, railsContext) => {
const { location } = railsContext;
// React on Rails calls RouterApp(props, railsContext) to get this function,
// then calls createElement(returnedFn, props). This example uses the props
// and location captured by closure.
return (_props) => (
<StaticRouter location={location}>
<RouterRoutes {...props} />
</StaticRouter>
);
};
export default RouterApp;
Note: Components rendered through
react_component_hashstill need the explicit object return shape instead of the render-function thunk shown here. Keep usingrenderToString(...)and return the object described in Render-Functions, for example{ renderedHtml: { componentHtml: renderToString(...), ...otherSlots } }.
If your app still uses the legacy shared Redux store, keep the provider around the StaticRouter.
File: app/javascript/src/HelloWorldApp/ror_components/HelloWorldApp.server.jsx
import React from 'react';
import { StaticRouter } from 'react-router-dom/server';
import { Provider } from 'react-redux';
import { Routes, Route } from 'react-router-dom';
import configureStore from '../store/helloWorldStore';
import HelloWorldContainer from '../containers/HelloWorldContainer';
// Import other components for routing
// import About from '../components/About';
// import Contact from '../components/Contact';
const HelloWorldApp = (props, railsContext) => {
const store = configureStore(props);
const { location } = railsContext;
return (_props) => (
<Provider store={store}>
<StaticRouter location={location}>
<Routes>
<Route path="/" element={<HelloWorldContainer />} />
</Routes>
</StaticRouter>
</Provider>
);
};
export default HelloWorldApp;
Important changes from React Router v5:
StaticRouter from 'react-router-dom/server' (not 'react-router')<Routes> and <Route> with element proplocation prop takes a string path from railsContextmatch() or RouterContext - simplified APIreact_component_hash still need the explicit object return shape
instead of the render-function thunk shown here. Keep using renderToString(...) and return the
object described in Render-Functions, for example
{ renderedHtml: { componentHtml: renderToString(...), ...otherSlots } }.Critical Step: To support direct URL visits, browser refresh, and server-side rendering, you must configure Rails to handle all React Router paths.
Add a wildcard route in your config/routes.rb:
# config/routes.rb
Rails.application.routes.draw do
# Your main route
get 'your_path', to: 'your_controller#index'
# Wildcard catch-all for React Router sub-routes
get 'your_path/*path', to: 'your_controller#index'
end
Example:
# For a HelloWorld app with React Router
get 'hello_world', to: 'hello_world#index'
get 'hello_world/*path', to: 'hello_world#index'
This configuration ensures:
/hello_world → Renders your React app/hello_world/about → Rails serves the same view, React Router handles routing/hello_world/contact → Rails serves the same view, React Router handles routingImportant: Your React Router paths should match your Rails route structure. If Rails serves your app at /hello_world, your React Router routes should start with /hello_world:
<Routes>
<Route path="/hello_world" element={<Home />} />
<Route path="/hello_world/about" element={<About />} />
<Route path="/hello_world/contact" element={<Contact />} />
</Routes>
For a complete example of React on Rails with React Router, see the React Webpack Rails Tutorial.
For a practical example of route organization, see the routes configuration file from the tutorial.
Note: This tutorial uses a legacy directory structure (client/app/bundles) from earlier React on Rails versions. Modern projects use app/javascript/src/ structure as shown in this guide. The React Router integration patterns remain applicable.
<Navigate> Component SSR BehaviorWhen using React Router's <Navigate> component with server-side rendering, be aware of the following behavior:
<Navigate> is a no-op. It renders null because useEffect does not fire on the server. No redirect is performed — Rails still sends the full response with whatever HTML was rendered.useEffect, which runs after hydration. This means:
Recommendations for SSR redirects:
redirect_to before rendering.<Navigate> only for client-side routing transitions where a brief flash is acceptable and SEO redirect semantics are not needed.