While developing application in ReactJs with Redux and React Router 4, what I felt is that, there is no direct relationship between application's state and routing. We needed to create those connections inside Components. In each component, we needed to check application state and based on that we made decisions where to go next(if needed).

Let's think of a scenario, where we need to call an API, which returns some data or 404. If it returns 404, we will redirect the user to a 404 page. So the traditional way of doing this is:

import React, { Component } from 'react'
import * as actions from "./actions"

class HelloWorld extends Component {
  componentWillRecieveProps(nextProps) {
      const { apiStatus, history } = nextProps
      if(apiStatus === "FAILED"){
          history.push("/not-found")
      }
  }
  
  componentWillMount(){
      this.props.fetchAPI(this.props.match.params.id)  // declaired in routes
  }

  render() {
    return (
      <div>
          <p>{this.props.apiStatus.data}</p>
      </div>
      )
  }
}

function mapToStateProps(state) {
  return {
    apiStatus: state.apiStatus
  }
}

export default connect(mapToStateProps, actions)(HelloWorld)

IMHO, this approach is bit messy, because in each component there is some codes regarding application flow, and needs to deal with history object to change routes. Let's try to get rid of this code.

Even if we want to remove this connection(between state and routing) from Component, we need to put it back somewhere else, and the best place is redux middleware. Because, it is able to intercept all the actions fired and access their payloads. It can even access redux store to fire actions and use reducers. But, there is a catch. We can't manipulate routing from there.

To overcome this hurdle, we need to use a library named connected-react-router. We can run npm install ---save connected-react-router or yarn add connected-react-router to install it. Also, need to follow their usage instructions for integrating this library with our react application.

Basically, we will be using the push method for updating our routing. We can fire actions with push method from middleware.

FYI: For the following example code, we are going to use redux-thunk, but if you are comfortable using redux-saga, then please checkout connected-react-router's example based on react-saga.

Now, let's write the middleware for routing:

import { push } from 'connected-react-router'

export const routingMiddleware = store => next => action => {
  if(action.type === API_FAILED){
      store.dispatch(push('/not-found'))  // assuming we are using redux thunk
  }
  return next(action)
}

And use it inside redux store:

import { routingMiddleware } from "./middlewares"

const store = createStore(
  connectRouter(history)(rootReducer),
  initialState,
  compose(
    applyMiddleware(
      routerMiddleware(history),
      routingMiddleware,
      // other middlewares
    ),
  ),
)

Also, let's not forget to remove routing related codes from Component. After that, we are ready to go!!

But, let us go one more step further, that is controlling the whole flow from middleware. For that, we need to create some actions:

export const GO_TO_PAGE_ONE = "GO_TO_PAGE_ONE"
export const GO_TO_PAGE_TWO = "GO_TO_PAGE_TWO"

export function goToPageOne(){
    return({
        type: GO_TO_PAGE_ONE
    })
}
export function goToPageTwo(){
    return({
        type: GO_TO_PAGE_TWO
    })
}

Add them in the Component:

import React, { Component } from 'react'
import * as actions from "./actions"

class HelloWorldAgain extends Component {
  render() {
    return (
      <div>
          <button onClick={this.props.goToPageOne()}>
              Go to Page One
          </button>
          <button onClick={this.props.goToPageTwo()}>
              Go to Page Two
          </button>
      </div>
      )
  }
}

export default connect(null, actions)(HelloWorldAgain);

Update the middleware, so it will catch any new actions when fired:

export const routingMiddleware = store => next => action => {
  if(action.type === API_FAILED){
      store.dispatch(push('/not-found'))
  } else if(action.type === GO_TO_PAGE_ONE){
      store.dispatch(push('/page-one'))
  } else if(action.type === GOTO_PAGE_TWO){
      store.dispatch(push('/page-two'))
  }
  return next(action)
}

Fantastic!! Now we are controlling our routing from middleware. We don't need to put any routing related code in the Component.

Wait, wait ... There is more to that. There is one more step we need to do, so that we can control the application flow in much better way. That  step is using reducers in middleware. Let's think of an example: before a user can go to Page Two, he/she needs to visit Page One first. Let's set up a reducer for accessing page one data:

import { GO_TO_PAGE_ONE } from '../actions';
import { combineReducers } from 'redux';

function pageOne(state = false, action) {
    switch (action.type) {
        case GO_TO_PAGE_ONE:
            return true
        default:
            return state
    }
    return state
}

export default combineReducer({
    pageOne: pageOne
});

Then, we can restrict the flow in middleware:

export const routingMiddleware = store => next => action => {
  if(action.type === API_FAILED){
      store.dispatch(push('/not-found'))
  } else if(action.type === GO_TO_PAGE_ONE){
      store.dispatch(push('/page-one'))
  } else if(action.type === GOTO_PAGE_TWO){
      if(store.getState().pageOne == true){
          store.dispatch(push('/page-two'))
      } else {
          store.dispatch(push('/not-found'))
      }
  }
  return next(action)
}

Thus, we can control our application flow from middleware!!

So far so good, right!! but there are some good side and bad side of this approach:

Pros:

  • All the flow restrictions and redirections in one place(One ring to rule them all !! ;) ). So change it in one place, reflect it everywhere else.
  • All the routing related codes are in middleware. Means application state will be reflected directly in routing without involving any Component.
  • No direct relation between Component and Routing (other than route declaration in store).
  • Component will not have any routing related codes. Means they will be less messy. Means we can detouch Components from Routing if we want.
  • Components can be more reusable.

Cons:

  • Performance could be slower than native implementation.
  • Bit of overengineering to acheive something simple.

Is there any better solution?

Well, when we were experimenting with this solution, we were almost at the end of our project, where we were heavily dependent on react router 4. But if you are only at the beginning or don't have that much regard for react router 4, then consider using redux first router.

Thank you for reading. Please let me know your feedback in comments section below.