Author avatar

Manujith Pallewatte

Firing Redux Actions on Route Transitions

Manujith Pallewatte

  • Jan 10, 2020
  • 7 Min read
  • 2,105 Views
  • Jan 10, 2020
  • 7 Min read
  • 2,105 Views
Web Development
React

Introduction

Routing is a key component in any complex React web application, and react-router is the most commonly used routing library for React. Although it performs perfectly for simple routing tasks, when handling complicated situations the solutions are not straightforward.

For example, in a blog application, you might want to create routes for viewing a list of blog posts and viewing a single blog post. The obvious design would be as follows:

  1. A PostList component will load the current set of blog posts from an API and show them in a list. The posts are stored in the app state.
  2. The user selects a post from the list.
  3. Assign the select post object to an activePost variable.
  4. Do a route transition to Post component, which essentially displays the content from the activePost variable.

This works well if the user always begins navigation from the PostList component. But a more robust app would allow the user to directly enter a specific URL and expect the app to display the corresponding post. For example, the user would enter http://myapp.com/posts/awesome-post and the app would navigate to Post component and display the content of the awesome-post article. Since the activePost variable is not set in this instance, the view would end up being empty.

In this guide, we'll explore solutions for these use cases. To generalize the scope of the guide, we'll focus on firing any Redux action on route transition rather than focusing on an API call.

Using Component Life Cycle

One solution is to handle actions in component life cycles. This solution would apply to instances where the action that is fired on route transition affects only the current components. In a class component, we can use componentDidMount combined with componentWillReceiveProps events to have the desired effect.

Let's assume a Redux store, configured as follows, along with an action that needs to be fired on route transition.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// reducer.js
import { ACTION_TO_FIRE } from './actions.js'

const initialState = {
 // ...
}

export default function appReducer(state, action) {
 if (typeof state === 'undefined') {
 return initialState
 }

 switch(action.type){
 case ACTION_TO_FIRE:
 // do something with the payload
 break;
 }
javascript
1
2
3
4
5
6
7
8
9
10
11
12
// actions.js

// These are our action types
export const ACTION_TO_FIRE = "ACTION_TO_FIRE"

// Now we define actions
export function fireThisAction(payload){
 return {
 type: ACTION_TO_FIRE,
 payload
 }
}
javascript

With this, let's have our class component that would fire the fireThisAction on route transition.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// AppComponent.js

import React, { Component } from 'react';
import { connect } from 'react-redux'
import { withRouter } from 'react-router'
import { fireThisAction } from './actions';

class AppComponent extends Component {
 componentDidMount(){
 fireThisAction();
 }

 componentWillReceiveProps (nextProps) {
 if (this.props.location.pathname === nextProps.location.pathname && 
 this.props.location.search === nextProps.location.search
 ) {

 // this means that no change in the path, so we don't need to refire the
 // action
 return;
 }

 // if not fire the action
 fireThisAction();
 }

 render(){
 return (
 // render the content here after loading data
 );
 }
}

function mapStateToProps(state) {
 return {
 
 }
}

const mapDispatchToProps = {
 fireThisAction 
};

export default withRouter(connect(mapStateToProps, mapDispatchToProps)(AppComponent));
javascript

As shown in the above code example, we first use componentDidMount to load data on route transition. But to make it work across all cases where the component is already mounted (ex: route change without unmounting the component), we need to override the componentWillReceiveProps event and see if we need to refire the action. We are simply comparing the full path of the current URL to determine whether to refire the action.

In a real-world application, you will find that you only need to compare a certain path parameter to determine this. If we consider our blog post example above, we need to check if the path parameter that indicates the post slug ("awesome-post") has changed. This is easily accomplished by comparing the contents of props.match.params.

Using an Event Listener

An alternative use case is where you need to fire a Redux action every time a route transition occurs. For instance, the app may need to call a logging API every time a user navigates to a set of pages. Instead of specifying the log call in each page component (as shown in the previous use case), we could use a global event listener to accomplish the task. The listener would be bound to the router history.

For clarity let's assume that the Redux store and the router history are available in the corresponding files, as below.

Note: This guide is targeting react-router-v4, the latest release in use. For previous react-router releases, this may slightly vary. In these scenarios, history must be replaced with browserHistory or hashHistory, depending on usage in the app.

1
2
3
4
5
6
7
8
// history.js
import createHistory from 'history/createBrowserHistory';

// this history object is used for the history parameter in our Router
// <Router history={history}> ... </Router>
const history = createHistory();

export default history;
javascript
1
2
3
4
5
6
7
// store.js
import { createStore } from 'redux'
import { appReducer } from './reducer.js'

const store = createStore(appReducer)

export default store;
javascript

Now we implement the event listener inside the history.js itself. The listener will be called each time a router transition occurs with the current location object as an argument.

1
2
3
4
5
6
7
8
9
10
11
12
// history.js
import createHistory from 'history/createBrowserHistory';
import store from './store.js'
import { fireThisAction } from './actions';

const history = createHistory();

history.listen((location) => {
 store.dispatch(fireThisAction(location));
})

export default history;
javascript

Inside the fireThisAction, the conditional tasks can be done. It must be noted that you could call any function within the listener.

Conclusion

Route transitions are inevitable in any real-world application. Often a transition occurs as a response to a user's action. In other instances, it could be triggered by a URL change by the user or the app itself. In this guide, we explored two ways in which route changes can be tracked from app components. Each method has its own use case and must be used accordingly.

2