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:
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.post
object to an activePost
variable.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.
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 }
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}
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));
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
.
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;
1// store.js
2import { createStore } from 'redux'
3import { appReducer } from './reducer.js'
4
5const store = createStore(appReducer)
6
7export default store;
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;
Inside the fireThisAction
, the conditional tasks can be done. It must be noted that you could call any function within the listener.
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.