connected-react-router vs react-router vs react-router-redux vs redux-first-history
Architectural Strategies for Routing and State Management in React
connected-react-routerreact-routerreact-router-reduxredux-first-historySimilar Packages:

Architectural Strategies for Routing and State Management in React

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.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
connected-react-router04,687444 kB176-MIT
react-router056,5742.8 MB18213 days agoMIT
react-router-redux07,737-110 years agoMIT
redux-first-history045499.3 kB183 years agoMIT

Routing Architecture in React: Standalone vs. Redux-Integrated Patterns

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.

πŸ›οΈ The Core Philosophy: Decoupled vs. Coupled State

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>

⚠️ Deprecation Warning: The Fate of react-router-redux

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

πŸ”„ Data Flow: Who Drives the URL?

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

πŸ› οΈ Implementation Complexity and Boilerplate

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>
  );
}

🧩 Real-World Use Cases

Scenario 1: Standard Web Application

You are building a marketing site, an e-commerce store, or a standard SaaS dashboard.

  • Best Choice: react-router
  • Why: It is the industry standard. It supports code-splitting, nested routes, and data loading natively. There is no benefit to adding Redux overhead to your routing logic.
// Standard implementation
<Routes>
  <Route path="/" element={<Layout />}>
    <Route index element={<Home />} />
    <Route path="products" element={<Products />} />
  </Route>
</Routes>

Scenario 2: Complex Time-Travel Debugging

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.

  • Best Choice: connected-react-router
  • Why: It stores the location 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

Scenario 3: Legacy Maintenance

You are stuck on an old codebase using React Router v3 and Redux, and a full rewrite is not possible this quarter.

  • Best Choice: react-router-redux (Temporary only)
  • Why: It is the only thing that works with that specific legacy stack. However, plan your migration to react-router v6 immediately.
// Legacy context only
// import { syncHistoryWithStore } from 'react-router-redux';
// Only acceptable in unmaintainable legacy constraints

Scenario 4: Experimental State-Driven Architecture

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.

  • Best Choice: redux-first-history
  • Why: It forces navigation to be an explicit action. This guarantees that every page view is a dispatched event that can be intercepted, logged, or rejected by middleware.
// Audit trail example
store.dispatch({
  type: '@@router/LOCATION_CHANGE',
  payload: { pathname: '/secure-area' },
  meta: { timestamp: Date.now(), userId: 123 }
});

πŸ“Š Summary Comparison

Featurereact-routerconnected-react-routerreact-router-reduxredux-first-history
Statusβœ… Active Standard⚠️ Maintenance Mode❌ DeprecatedπŸ§ͺ Experimental
Source of TruthBrowser HistoryBrowser History (Synced)Browser History (Legacy)Redux Store
Setup ComplexityLowMediumLow (Legacy)High
Time TravelNoYesYes (Legacy)Yes
PerformanceHighHigh (Small Overhead)N/ALower (Extra Dispatch)
RecommendationDefault ChoiceLegacy DebuggingDo Not UseNiche Cases Only

πŸ’‘ The Architectural Verdict

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.

How to Choose: connected-react-router vs react-router vs react-router-redux vs redux-first-history

  • connected-react-router:

    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.

  • react-router:

    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.

  • react-router-redux:

    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.

  • redux-first-history:

    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.

README for connected-react-router

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.

Connected React Router Build Status Open Source Helpers

A Redux binding for React Router v4 and v5

Main features

: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

Installation

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

Usage

Step 1

In your root reducer file,

  • Create a function that takes history as an argument and returns a root reducer.
  • Add router reducer into root reducer by passing history to connectRouter.
  • Note: The key MUST be 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

Step 2

When creating a Redux store,

  • Create a history object.
  • Provide the created history to the root reducer creator.
  • Use 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
}

Step 3

  • Wrap your react-router v4/v5 routing with 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.
  • Place ConnectedRouter as a child of react-redux's Provider.
  • N.B. If doing server-side rendering, you should still use the 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!

Examples

See the examples folder

FAQ

Build

npm run build

Generated files will be in the lib folder.

Development

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>
  )
}
...

Contributors

See Contributors and Acknowledge.

License

MIT License