This comparison analyzes four distinct approaches to handling routing in React applications, specifically focusing on their relationship with Redux state management. react-router is the industry-standard library for declarative routing that manages history and UI rendering independently. react-router-redux (now deprecated) was an early attempt to sync router state into Redux for time-travel debugging. connected-react-router evolved this concept to support React Router v4+ by keeping the router state in sync with Redux while maintaining separate history objects. redux-first-history represents a paradigm shift where the Redux store becomes the single source of truth for the URL, driving routing changes directly through dispatched actions rather than a separate history listener.
Managing navigation in React applications often boils down to a critical architectural decision: should the router live independently, or should it be tightly coupled with your global state manager (Redux)? The packages react-router, connected-react-router, react-router-redux, and redux-first-history represent the evolution of this debate. Let's break down how they work, why some are obsolete, and where the edge cases still exist.
react-router treats routing as a separate concern. It manages its own internal state and listens to browser history events directly. The URL drives the UI, but the URL is not stored in your Redux store.
// react-router: Standard usage
import { BrowserRouter, Routes, Route } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</BrowserRouter>
);
}
// Navigation is handled via hooks or components, not Redux dispatch
connected-react-router bridges the gap. It keeps react-router's history object but syncs the current location into the Redux store. This allows you to see route changes in Redux DevTools.
// connected-react-router: Syncing state
import { ConnectedRouter } from 'connected-react-router';
import { createBrowserHistory } from 'history';
const history = createBrowserHistory();
function Root() {
return (
<Provider store={store}>
<ConnectedRouter history={history}>
<Routes>...</Routes>
</ConnectedRouter>
</Provider>
);
}
// Location state is now available at state.router.location
react-router-redux was the original attempt at this sync. It tried to make the Redux store the single source of truth but struggled with the architectural shifts in React Router v4. It is no longer viable.
// react-router-redux: DEPRECATED PATTERN
// DO NOT USE - This API is obsolete and incompatible with v4+
import { routerReducer } from 'react-router-redux';
// This package has been unmaintained for years
redux-first-history flips the model entirely. There is no separate history listener. You dispatch a Redux action like { type: '@@router/LOCATION_CHANGE', payload: ... }, and the router updates because the store changed.
// redux-first-history: Action-driven routing
import { push } from 'redux-first-history';
// Dispatch an action to navigate
dispatch(push('/dashboard'));
// The router component subscribes to the store to render
<ReduxFirstHistory store={store}>
<Routes>...</Routes>
</ReduxFirstHistory>
It is critical to state clearly: react-router-redux is dead. It was built for React Router v2 and v3. When React Router v4 introduced a component-based API, react-router-redux could not adapt without a complete rewrite. The maintainers officially deprecated it and recommended moving to connected-react-router or simply using react-router standalone.
If you encounter react-router-redux in a codebase today, it is a technical debt signal. You cannot use it with modern React Router features like nested layouts or data loaders. Migration is mandatory.
// β WRONG: Using deprecated package
import { routerMiddleware } from 'react-router-redux';
// β
RIGHT: Migrating to connected-react-router or standalone
import { connectRouter } from 'connected-react-router';
// OR simply remove Redux integration entirely
The fundamental difference lies in the direction of data flow.
In react-router and connected-react-router, the Browser History API is the master. When you click a link, the browser URL changes, the history library notifies the router, and the UI updates. Redux (if used) is just a passive observer in connected-react-router.
// Flow: Browser Event -> History Lib -> React Router -> UI -> (Optional) Redux Sync
<Link to="/about">About</Link>
// Clicking this triggers native browser pushState
In redux-first-history, the Redux Store is the master. The browser history is a side effect. You must dispatch an action to change the URL. If your Redux update fails or is blocked by middleware, the navigation never happens.
// Flow: Action Dispatch -> Redux Store Update -> Router Subscriber -> Browser History API
dispatch({ type: 'NAVIGATE', path: '/about' });
// Navigation only occurs if the reducer accepts this state change
react-router requires the least setup. You wrap your app, define routes, and go. It leverages React Context heavily, making it easy to access navigation data anywhere via hooks.
// react-router: Minimal setup
import { useNavigate } from 'react-router-dom';
function MyComponent() {
const navigate = useNavigate();
const handleClick = () => navigate('/home');
return <button onClick={handleClick}>Go Home</button>;
}
connected-react-router adds moderate complexity. You must create a history object manually, pass it to both the Redux enhancer and the React component, and ensure they stay in sync. This introduces a potential source of bugs if the history object is recreated accidentally.
// connected-react-router: Manual history management
import { createBrowserHistory } from 'history';
const history = createBrowserHistory(); // Must be a singleton
// Must be passed to both store and component
const store = createStore(rootReducer, applyMiddleware(routerMiddleware(history)));
// <ConnectedRouter history={history}>...
redux-first-history introduces high complexity. You lose standard <Link> components unless you wrap them to dispatch actions. Debugging navigation issues requires tracing Redux action logs rather than checking the browser network tab or history stack directly.
// redux-first-history: Custom Link implementation
function ReduxLink({ to, children }) {
const dispatch = useDispatch();
return (
<a onClick={(e) => {
e.preventDefault();
dispatch(push(to)); // Manual dispatch required
}}>
{children}
</a>
);
}
You are building a marketing site, an e-commerce store, or a standard SaaS dashboard.
react-router// Standard implementation
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="products" element={<Products />} />
</Route>
</Routes>
You are building a collaborative editing tool or a complex simulation where the ability to rewind the entire application state (including the URL) to a previous point is a core requirement.
connected-react-routerlocation object in the Redux tree. When you rewind the Redux state, the router rewinds with it, restoring the URL and the associated UI state perfectly.// Accessing location from Redux for debugging
const location = useSelector(state => state.router.location);
// This state is now part of the time-travel snapshot
You are stuck on an old codebase using React Router v3 and Redux, and a full rewrite is not possible this quarter.
react-router-redux (Temporary only)react-router v6 immediately.// Legacy context only
// import { syncHistoryWithStore } from 'react-router-redux';
// Only acceptable in unmaintainable legacy constraints
You are researching a pattern where every single change in the app, including navigation, must be logged as a discrete event for audit trails or replay systems.
redux-first-history// Audit trail example
store.dispatch({
type: '@@router/LOCATION_CHANGE',
payload: { pathname: '/secure-area' },
meta: { timestamp: Date.now(), userId: 123 }
});
| Feature | react-router | connected-react-router | react-router-redux | redux-first-history |
|---|---|---|---|---|
| Status | β Active Standard | β οΈ Maintenance Mode | β Deprecated | π§ͺ Experimental |
| Source of Truth | Browser History | Browser History (Synced) | Browser History (Legacy) | Redux Store |
| Setup Complexity | Low | Medium | Low (Legacy) | High |
| Time Travel | No | Yes | Yes (Legacy) | Yes |
| Performance | High | High (Small Overhead) | N/A | Lower (Extra Dispatch) |
| Recommendation | Default Choice | Legacy Debugging | Do Not Use | Niche Cases Only |
For 95% of applications, react-router is the correct choice. The industry has moved away from putting everything in Redux. Modern React patterns favor colocating state and using context for things like routing. Adding Redux to the routing path introduces unnecessary boilerplate and potential performance bottlenecks without providing tangible benefits for most UIs.
Reserve connected-react-router for very specific legacy scenarios where time-travel debugging of the URL is a non-negotiable requirement. Avoid react-router-redux entirely as it is obsolete. Only consider redux-first-history if you are building a highly specialized system where the "Redux as single source of truth" philosophy must be applied rigorously to navigation, and you are willing to pay the complexity tax.
Final Thought: Routing is about moving the user through your application. Keep it simple, keep it standard, and let the browser do what it does best unless you have a compelling reason to intervene.
Choose connected-react-router only if you are maintaining a legacy application that strictly requires the URL state to be preserved inside the Redux store for complex time-travel debugging or specific middleware logic, and you cannot migrate to modern patterns.
Choose react-router for almost all modern React projects. It is the standard, actively maintained solution that decouples routing logic from global state, offering the best performance and simplest API for nested routes, layout groups, and data loading via loaders.
Do NOT choose react-router-redux for any new or existing project. It is officially deprecated and incompatible with modern React Router versions (v4 and above). You must migrate to react-router or connected-react-router immediately.
Choose redux-first-history only for highly experimental architectures where you need to treat navigation purely as a state transition driven by Redux actions, accepting the significant complexity and loss of standard browser history features as a trade-off.
Breaking change in v5.0.0! Please read How to migrate from v4 to v5/v6.
v6.0.0 requires React v16.4.0 and React Redux v6.0 / v7.0.
A Redux binding for React Router v4 and v5
:sparkles: Synchronize router state with redux store through uni-directional flow (i.e. history -> store -> router -> components).
:gift: Supports React Router v4 and v5.
:sunny: Supports functional component hot reloading while preserving state (with react-hot-reload).
:tada: Dispatching of history methods (push, replace, go, goBack, goForward) works for both redux-thunk and redux-saga.
:snowman: Nested children can access routing state such as the current location directly with react-redux's connect.
:clock9: Supports time traveling in Redux DevTools.
:gem: Supports Immutable.js
:muscle: Supports TypeScript
Connected React Router requires React 16.4 and React Redux 6.0 or later.
npm install --save connected-react-router
Or
yarn add connected-react-router
In your root reducer file,
history as an argument and returns a root reducer.router reducer into root reducer by passing history to connectRouter.router.// reducers.js
import { combineReducers } from 'redux'
import { connectRouter } from 'connected-react-router'
const createRootReducer = (history) => combineReducers({
router: connectRouter(history),
... // rest of your reducers
})
export default createRootReducer
When creating a Redux store,
history object.history to the root reducer creator.routerMiddleware(history) if you want to dispatch history actions (e.g. to change URL with push('/path/to/somewhere')).// configureStore.js
...
import { createBrowserHistory } from 'history'
import { applyMiddleware, compose, createStore } from 'redux'
import { routerMiddleware } from 'connected-react-router'
import createRootReducer from './reducers'
...
export const history = createBrowserHistory()
export default function configureStore(preloadedState) {
const store = createStore(
createRootReducer(history), // root reducer with router state
preloadedState,
compose(
applyMiddleware(
routerMiddleware(history), // for dispatching history actions
// ... other middlewares ...
),
),
)
return store
}
ConnectedRouter and pass the history object as a prop. Remember to delete any usage of BrowserRouter or NativeRouter as leaving this in will cause problems synchronising the state.ConnectedRouter as a child of react-redux's Provider.StaticRouter from react-router on the server.// index.js
...
import { Provider } from 'react-redux'
import { Route, Switch } from 'react-router' // react-router v4/v5
import { ConnectedRouter } from 'connected-react-router'
import configureStore, { history } from './configureStore'
...
const store = configureStore(/* provide initial state if any */)
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}> { /* place ConnectedRouter under Provider */ }
<> { /* your usual react-router v4/v5 routing */ }
<Switch>
<Route exact path="/" render={() => (<div>Match</div>)} />
<Route render={() => (<div>Miss</div>)} />
</Switch>
</>
</ConnectedRouter>
</Provider>,
document.getElementById('react-root')
)
Note: the history object provided to router reducer, routerMiddleware, and ConnectedRouter component must be the same history object.
Now, it's ready to work!
See the examples folder
npm run build
Generated files will be in the lib folder.
When testing the example apps with npm link or yarn link, you should explicitly provide the same Context to both Provider and ConnectedRouter to make sure that the ConnectedRouter doesn't pick up a different ReactReduxContext from a different node_modules folder.
In index.js.
...
import { Provider, ReactReduxContext } from 'react-redux'
...
<Provider store={store} context={ReactReduxContext}>
<App history={history} context={ReactReduxContext} />
</Provider>
...
In App.js,
...
const App = ({ history, context }) => {
return (
<ConnectedRouter history={history} context={context}>
{ routes }
</ConnectedRouter>
)
}
...
See Contributors and Acknowledge.