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.
There are different libraries and methods available to integrate the routing state with Redux. The most commonly used are:
push
, replace
, go
, goBack
, and goForward
for convenient navigation across your application.
BrowserRouter
, Link
, Route
, and Switch
, all of which make its integration seamless
state.router.location
methodIf you are interested in exploring other libraries that integrate routing with Redux, you can check out this GitHub repo for more insights.
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.
There are two ways that the user can navigate through the application: internally and externally.
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.
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.
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
store.js
fileLet’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.
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.
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 }
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 pagesFinally, 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)
Storing routing state in Redux may be a good option in some scenarios, such as when you want to:
However, there are a lot of problems that come along with it.
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.
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.
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.
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.
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.
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, such as navigating back and forth on our application route, come out of the box in React Router.
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.
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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ 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>
Hey there, want to help make our blog better?
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.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
2 Replies to "Why you don’t need to mix routing state with Redux"
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.