Author avatar

Manujith Pallewatte

Firing Redux Actions on Route Transitions

Manujith Pallewatte

  • Jan 10, 2020
  • 7 Min read
  • 9,642 Views
  • Jan 10, 2020
  • 7 Min read
  • 9,642 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// reducer.js
2import { ACTION_TO_FIRE } from './actions.js'
3
4const initialState = {
5 // ...
6}
7
8export default function appReducer(state, action) {
9 if (typeof state === 'undefined') {
10 return initialState
11 }
12
13 switch(action.type){
14 case ACTION_TO_FIRE:
15 // do something with the payload
16 break;
17 }
javascript
1// actions.js
2
3// These are our action types
4export const ACTION_TO_FIRE = "ACTION_TO_FIRE"
5
6// Now we define actions
7export function fireThisAction(payload){
8 return {
9 type: ACTION_TO_FIRE,
10 payload
11 }
12}
javascript

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

1// AppComponent.js
2
3import React, { Component } from 'react';
4import { connect } from 'react-redux'
5import { withRouter } from 'react-router'
6import { fireThisAction } from './actions';
7
8class AppComponent extends Component {
9 componentDidMount(){
10 fireThisAction();
11 }
12
13 componentWillReceiveProps (nextProps) {
14 if (this.props.location.pathname === nextProps.location.pathname && 
15 this.props.location.search === nextProps.location.search
16 ) {
17
18 // this means that no change in the path, so we don't need to refire the
19 // action
20 return;
21 }
22
23 // if not fire the action
24 fireThisAction();
25 }
26
27 render(){
28 return (
29 // render the content here after loading data
30 );
31 }
32}
33
34function mapStateToProps(state) {
35 return {
36 
37 }
38}
39
40const mapDispatchToProps = {
41 fireThisAction 
42};
43
44export 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// history.js
2import createHistory from 'history/createBrowserHistory';
3
4// this history object is used for the history parameter in our Router
5// <Router history={history}> ... </Router>
6const history = createHistory();
7
8export default history;
javascript
1// store.js
2import { createStore } from 'redux'
3import { appReducer } from './reducer.js'
4
5const store = createStore(appReducer)
6
7export 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// history.js
2import createHistory from 'history/createBrowserHistory';
3import store from './store.js'
4import { fireThisAction } from './actions';
5
6const history = createHistory();
7
8history.listen((location) => {
9 store.dispatch(fireThisAction(location));
10})
11
12export 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.