Editor’s note: This post was updated on 7 December 2021 to remove references to the deprecated react-router-redux package, rework the tutorial with the official react-router-redux replacement, improve the descriptions of dynamic routing, and provide more clarity on when complexity should be expected.
This article covers some of the reasons why you don’t need to mix routing state with Redux.
Before we start with the reasons you don’t need to mix the routing state, let’s first review the available ways to integrate the routing state with Redux and understand how it works.
Popular routing state libraries for React
There are different libraries and methods available to integrate the routing state with Redux. The most commonly used are:
- connected-react-router (formerly known as the deprecated react-router-redux): This library is suitable when you want to synchronize your route history with your app state and manage routing through Redux. It provides some features like
push
,replace
,go
,goBack
, andgoForward
for convenient navigation across your application.- React-router-dom: This is a highly regarded library for implementing navigation and routing in a React application that comes with features such as the
BrowserRouter
,Link
,Route
, andSwitch
, all of which make its integration seamless- Redux-first-router: Another React library, this gives you access to navigate across your browser history and the current location/URL of the user within your application
- Redux-first-history: The developers of this library’s main goal is to enable users of the library to mix components that obtain the navigation history from any other library through its
state.router.location
method
- React-router-dom: This is a highly regarded library for implementing navigation and routing in a React application that comes with features such as the
If you are interested in exploring other libraries that integrate routing with Redux, you can check out this GitHub repo for more insights.
How does Redux routing work?
Redux is a state management tool that keeps the data or state within and across your application in sync. It achieves this through a single location where the states of your components reside. This single location is regarded as the Store.
An action with the potential to change the state of your application will be dispatched to this Store and the state will be updated. Any component within your application that subscribes to this state will be notified of the new state of the component and the value will be rendered through that component.
Typically, the browser history and the Redux store are in constant communication with each other to stay in sync. Each time the user navigates through the application, the location change is updated in both the browser history and the Store.
This may seem to go against the “single source of truth” principle of Redux, but that is not the case. As long as you can guarantee that the routing data stored in the browser history and the Redux Store are the same, you can configure your application to fetch data from the store only, thereby maintaining the single source of truth principle.
Navigating through your application
There are two ways that the user can navigate through the application: internally and externally.
Internal navigation
This type of navigation occurs when the user clicks a link within our application, such as the Contact tab/button on the navbar or any link that routes the user to another page of the application.
This is usually handled by the history
feature of the library that manages the routing of your application. The Redux middleware receives the action and updates the browser history along with the reducer, which updates the Redux state.
After that, our connected route listens for the change of state and determines how the page renders based on the Redux state.
External navigation
A good example of this is visiting a webpage from another website, either through the URL bar, an external link, or when you navigate back and forth through pages using the navigation button of your browser. Simply put, accessing a webpage through the navigation bar of your browser is considered external navigation.
When the URL is changed in the browser, our listener in the Redux Store observes the change and dispatches an action to update the state and history.
Implementing the Redux-first routing approach
Let’s explore a simple example of the Redux-first routing approach. This will help you to understand how it is implemented in our application.
Run the line of code below in your terminal to create a React app:
npx create-react-app redux-first-demo
cd
into the React app and install the redux-first-router
library.
cd redux-first-demo npm i redux-first-router
Configuring the store.js
file
Let’s begin by creating a file where we configure the store that holds the state of the application. Create a file named store.js
and add the following code snippet:
import { applyMiddleware, combineReducers, compose, createStore } from 'redux' import { connectRoutes } from 'redux-first-router' import page from './pageReducer' const routesMap = { HOME: '/', USER: '/user/:id' } export default function store(preloadedState) { const { reducer, middleware, enhancer } = connectRoutes(routesMap) const rootReducer = combineReducers({ page, location: reducer }) const middlewares = applyMiddleware(middleware) const enhancers = compose(enhancer, middlewares) const store = createStore(rootReducer, preloadedState, enhancers) return { store } }
In the store.js
file above, connectRoutes
maps the router to the components we want to render. The routesMap
is an object that contains the paths and the key to the respective components they render.
Then, we initialize the store with the createStore
API, using the processed values of the combineReducers
and applyMiddleware
APIs.
Updating state with pageReducer.js
The reducer dispatches the Redux action that updates the state of the application. The code below is an implementation of the reducer
function for our application, which contains the components that need to be rendered based on the route.
Here, we check the type of action passed to the reducer. For instance, if the action is of type HOME
, we return its state.
More great articles from LogRocket:
- Don't miss a moment with The Replay, a curated newsletter from LogRocket
- Learn how LogRocket's Galileo cuts through the noise to proactively resolve issues in your app
- Use React's useEffect to optimize your application's performance
- Switch between multiple versions of Node
- Discover how to animate your React app with AnimXYZ
- Explore Tauri, a new framework for building binaries
- Advisory boards aren’t just for executives. Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
import { NOT_FOUND } from 'redux-first-router' const components = { HOME: 'Home', USER: 'User', [NOT_FOUND]: 'NotFound' } export default (state = 'HOME', action = {}) => { return components[action.type] || state }
Rendering the React components
The components.js
file contains the components available for us to render in our React application.
import React from 'react' import { connect } from 'react-redux' const Home = () => <h3>Home</h3> const User = ({ userId }) => <h3>{`User ${userId}`}</h3> const mapStateToProps = ({ location }) => ({ userId: location.payload.id }) const ConnectedUser = connect(mapStateToProps)(User) const NotFound = () => <h3>404</h3> export { Home, ConnectedUser as User, NotFound }
App.js
helps load the right pages
Finally, the App.js
file is where the state of Redux’s page
argument determines the component to load based on the navigation state. We import all the components from the components.js
file and render their respective contents whenever the user navigates to the corresponding routes.
import React from 'react' import { connect } from 'react-redux' // Contains 'Home', 'User' and 'NotFound' import * as components from './components'; const App = ({ page }) => { const Component = components[page] return <Component /> } const mapStateToProps = ({ page }) => ({ page }) export default connect(mapStateToProps)(App)
Why you don’t need Redux for routing
Storing routing state in Redux may be a good option in some scenarios, such as when you want to:
- Access the history instance of the routing state
- Prevent a component from serving as an intermediary between the routing state and app state
- Navigate through your app using dispatched actions
- Leverage travel debugging when changing routes with Redux devtools
- Keep the routing data and store data in sync
However, there are a lot of problems that come along with it.
Complexity
One of the major problems that you’ll face while having routing-state in Redux is complexity.
You can’t predict how complicated it will be and your complete application state will rely on Redux.
For a few of us with heavy codebases, this complexity can be a good thing. Large applications will most likely be broken down into many components, thereby making the code more readable — which is a good advantage, but you’d have a lot more on your hands to deal with in terms of debugging and tracking performances.
You’d also have to manage everything in one place, which can be difficult to scale as your application starts to grow. In my opinion, it is unnecessary — it would be like managing all your components’ state in one place. Think about how hard that will be when your codebase grows.
Multiple “sources of truth”
One of the most prominent attributes of Redux is its single source of truth principle. Obeying this principle could be a hassle when you integrate routing with Redux because the Redux Store does not hold the information about your URL and navigation history — this is handled by the React Router library and the routing components.
Since the current location of the URL also determines what data will be rendered on the view, this implies that you’d also have to consider the data supplied by the routing components in addition to the Redux Store.
In order to maintain the Redux Store as your main source for fetching accurate data, you’d have to write code that keeps track of the data between the store and the router, and keep them in sync as the user interacts with your React app.
Verbosity
Another problem that you might need to handle is that you’ll end up with a lot of code for solving simple problems. You might need to write a lot of code just to navigate to a page when this could be avoided easily.
You have to manage all the actions and reducers just for routing, along with the middleware to update the browser history API for routing.
Code redundancy
You could end up writing lots of redundant code if you use Redux for routing, which could be avoided easily. For example, you might need to write a lot of actions and reducer functions to handle the routing functionality.
This may give you some power to control the router logic on your own, but you may not need that power to handle most of the application’s requirements.
So, you might end up writing code that could be simplified if you used client-side routing.
Alternatives to Redux routing
One of the popular ways of managing routing problems in the React ecosystem is react-router, which was briefly described at the beginning of this article. It is a client-side router that solves most of the problems we face when developing React applications.
Let’s look at some of the advantages of the React Router library.
Dynamic route-matching
Using React Router, we can match dynamic routes with React components. Consider that you have an application requirement for a dynamic subdomain:
logrocket.slack.com
Here, the subdomain changes dynamically. We can handle that route using React Router easily. We can also perform some actions based on the subdomain using React Router without a need to go to Redux.
Browser history features
Browser history features, such as navigating back and forth on our application route, come out of the box in React Router.
Lazy loading
React Router supports lazy loading. This helps you split your code bundle based on priority. You can load the primary features in the top bundle, and load the secondary features in the split bundles.
Final thoughts
At the end of the day, all that matters is the problem that we solve. Most importantly, we need to do that simply and efficiently, and there will be some benefit to using the Redux-first routing approach.
But we can solve the same problem using the simpler means that we discussed in this article. There are a lot of libraries that help us to do that, such as React Router.
Get setup with LogRocket's modern React error tracking in minutes:
- Visit https://logrocket.com/signup/ to get an app ID.
- Install LogRocket via NPM or script tag.
LogRocket.init()
must be called client-side, not server-side. - (Optional) Install plugins for deeper integrations with your stack:
- Redux middleware
- ngrx middleware
- Vuex plugin
$ npm i --save logrocket
// Code:
import LogRocket from 'logrocket';
LogRocket.init('app/id');
Add to your HTML:
<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
I can’t agree less.
Redux is all about managing the entire state in a scalable way. Its useful to have the current page and parameters in the store instead of in an additional place accessed and modified in a different way.
Its actually less complex.
It is dynamic, you imply it isn’t.
Browser history is supported by redux easily therefore the state of the navigation as well.
Absolutely true. I think the biggest problem with all these articles saying redux is bad for X or Y simply didnt understand how to work with redux. Its super simple and VERY powerful. I just don’t get people who say anything else.