Author avatar

Gaurav Singhal

How to Abort a Request While Navigating Away From the Component in React

Gaurav Singhal

  • Dec 20, 2019
  • 13 Min read
  • 7,198 Views
  • Dec 20, 2019
  • 13 Min read
  • 7,198 Views
Web Development
React

Introduction

React-Redux is one of the more popular library combinations. One of the advantages is that it's very flexible and we can write our code independently of the library. This flexibility also means that we should cover all the edge cases that may not be included in the library's code.

One such edge case is aborting a network request when the user navigates away from the current page component. If we do not abort or cancel the request, it can prove to be very costly for performance, especially when the user is on a low-end device or slow connection.

In this guide, we are going to learn how to handle this scenario b creating custom middlewares in Redux for managing the network request.

Setting up the Project

We'll run the create-react-app command to create our React project.

1
create-react-app cancel-demo
console

Next, we will install the required npm modules.

1
npm i react-redux redux axios react-router-dom
console

By now you should be familiar with axios and react-router. axios is an HTTP library for handling network requests and react-router is a routing solution for single page react applications.

Setting up the Basic Components

We will create two routes: Home and Search .

The <Home /> component is going to be a simple component with some text, and in the <Search /> component we will create a search input and connect it to Redux to make the API request. For this demo, I'm going to use the Pixabay API; you can choose any other public API as well.

Home.js

1
2
3
4
5
6
7
8
import React from "react";

export default props => (
  <div>
    <p>This is the Home Page.</p>
    <p>Go to Search Page and search some images.</p>
  </div>
);
jsx

Search.js

In the <Search /> component, we are going to store the searched term in the state, and on the search submission, we are going to dispatch the action to search for the images. All the action creators will be shown in an upcoming section.

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
import React, { Component } from "react";
import { connect } from "react-redux";
import { search } from "./actions/search";

class Search extends Component {
  constructor(props) {
    super(props);
    this.state = {
      searchTerm: "",
      images: []
    };
  }

  render() {
    return (
      <div>
        <input
          value={this.state.searchTerm}
          onChange={e => this.setState({ searchTerm: e.target.value })}
        />
        <button onClick={() => this.props.search(this.state.searchTerm)}>
          Search
        </button>
      </div>
    );
  }
}

export default connect(null, {
  search
})(Search);
jsx

Connecting React-Router to Redux Store

Now, let's connect react-router to redux store. Redux will be the single point of truth for the app's data and React Router will be the single point of truth for the URLs.

We will import BrowserRouter, Switch, and Route from react-router-dom .

The <BrowserRouter /> uses the HTML5 History API to keep the UI in sync with the URLs.

The <Route /> component is the most crucial component in the react-router ecosystem. It renders a UI based on the current URL path.

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
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";

import { Provider } from "react-redux";
import { createStore } from "redux";

import reducer from "./reducer";

import Home from "./Home";
import Search from "./Search";
import Navbar from "./Navbar";

import { appMiddleware } from "./middlewares/app";
import { apiMiddleware } from "./middlewares/core";

const store = createStore(reducer);

function App() {
  return (
    <Provider store={store}>
      <Router>
        <div>
          <Navbar />
          <Switch>
            <Route path="/search">
              <Search />
            </Route>
            <Route path="/" default>
              <Home />
            </Route>
          </Switch>
        </div>
      </Router>
    </Provider>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
jsx

Write the Action Creators

An action creator is a function that creates an action. In more technical terms, it returns an action object that contains a type property and other payload data.

So in our example, we will separate the action creators in 3 files, api.js, ui.js, and search.js. In api.js we will include all the action creators that dispatch actions for network requests, in ui.js we will only have a single action creator whose sole purpose is to set the loader state to true or false, and in search.js, the action creators will dispatch actions to modify the search results.

api.js

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
// action types
export const API_REQUEST = "API_REQUEST";
export const API_SUCCESS = "API_SUCCESS";
export const API_ERROR = "API_ERROR";
export const CANCEL_API_REQUEST = "CANCEL_API_REQUEST";

// action creators
export const apiRequest = ({ url, method }) => {
  return {
    type: API_REQUEST,
    meta: { url, method }
  };
};

export const cancelApiRequest = () => {
  return {
    type: CANCEL_API_REQUEST
  };
};

export const apiSuccess = ({ response }) => ({
  type: API_SUCCESS,
  payload: response
});

export const apiError = ({ error }) => ({
  type: API_ERROR,
  payload: error
});
js

ui.js

1
2
3
4
5
6
export const SET_LOADER = "SET_LOADER";

export const setLoader = ({ state }) => ({
  type: SET_LOADER,
  payload: state
});
js

search.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export const SEARCH = "SEARCH";
export const CANCEL_SEARCH = "CANCEL_SEARCH";

export const search = term => {
  return {
    type: SEARCH,
    payload: term
  };
};

export const cancelSearch = () => {
  return {
    type: CANCEL_SEARCH
  };
};
js

Writing Custom Redux Middlewares

This is going to be an interesting section as we learn how to write our Redux middlewares.

Redux Middleware

A Redux middleware provides an interface through which we can modify and interact with actions that have been dispatched before they hit the Redux store. Redux middlewares can be used to log actions, report errors, making asynchronous requests, etc. Another important functionality of middlewares is that they can dispatch new actions. We are going to explore this pattern in more depth in our use case.

Before going into further details, let's first look at how to create a middleware.

A basic middleware function receives an action and interacts with it in some manner.

1
2
3
const middlewareFunction = action => {
  // ...
};
js

For the middleware function to dispatch the next action in the middleware chain, since a redux app can have any number of middlewares, it should be wrapped in another function, which receives a next dispatch parameter.

1
2
3
4
5
6
const wrappedMiddlewareFunction = next => {
  const middlewareFunction = action => {
    // ...
  };
  return middlewareFunction;
};
js

Simplifiying the above snippet we get,

1
2
3
const middlewareFunction = next => action => {
  // ...
};
js

Furthermore, the middleware function also receives a copy of the store, i.e., a pseudo store from the applyMiddleware function of redux. Once again, this is achieved by a wrapper function. So the complete template of the middleware function looks something like the below.

1
2
3
const middlewareFunction = store => next => action => {
  // ...
};
js

If we do not call the next method with the action, then the control won't reach the next middleware function.

1
2
3
const middlewareFunction = store => next => action => {
  next(action);
};
js

For our use case, I'm going to divide the middleware functions into to two categories, core middleware and app middleware. The core middleware is responsible for handling external API requests, and the app middleware is responsible for handling the app-specific functions, like in this case, search.

We will extract the dispatch method from the pseudo store so that we can dispatch actions.

To cancel an axios request, we first need to extract the cancel token from axios.CancelToken, get the source by calling the source() method of the cancel token, and then pass the source in the config of the axios call.

1
2
3
4
5
const CancelToken = axios.CancelToken;
const source = CancelToken.source();

// pass the source to axios call
axios(method, url, { cancelToken: source.token });
js

core.js

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
import axios from "axios";
import {
  API_REQUEST,
  apiError,
  apiSuccess,
  CANCEL_API_REQUEST
} from "../actions/api";

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

export const apiMiddleware = ({ dispatch }) => next => action => {
  next(action);

  if (action.type === API_REQUEST) {
    const { url, method } = action.meta;
    axios({
      method,
      url,
      cancelToken: source.token
    })
      .then(({ data }) => dispatch(apiSuccess({ response: data })))
      .catch(error => {
        console.log(error);
        if (axios.isCancel(error)) {
          console.log(error.message);
        } else {
          dispatch(apiError({ error: error.response.data }));
        }
      });
  } else if (action.type === CANCEL_API_REQUEST) {
    source.cancel("Operation canceled by the user.");
    console.log("REQUEST CANCELLED!!!");
  }
};
js

app.js

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
import { SEARCH, CANCEL_SEARCH } from "../actions/search";
import { apiRequest, cancelApiRequest } from "../actions/api";
import { setLoader } from "../actions/ui";

const PIXABAY_KEY = "14545036-912e59631b7d8e4d4ebbffc6a";

const PIXABAY_URL = `https://pixabay.com/api/?key=${PIXABAY_KEY}`;

export const appMiddleware = () => next => action => {
  next(action);
  switch (action.type) {
    case SEARCH: {
      next(
        apiRequest({
          url: `${PIXABAY_URL}&q=${action.payload}`,
          method: "GET"
        })
      );
      next(setLoader({ state: true }));
      break;
    }
    case CANCEL_SEARCH: {
      next(cancelApiRequest());
      next(setLoader({ state: false }));
      break;
    }
    default:
      break;
  }
};
js

Dispatching the Cancel Request

We will dispatch the cancel request action from the <NavBar /> component when the user clicks on a link.

Navbar.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { Component } from "react";
import { connect } from "react-redux";
import { cancelSearch } from "./actions/search";
import { Link } from "react-router-dom";

class NavBar extends Component {
  render() {
    return (
      <nav>
        <ul>
          <li onClick={() => this.props.cancelSearch()}>
            <Link to="/">Home</Link>
          </li>
          <li onClick={() => this.props.cancelSearch()}>
            <Link to="/search">Search</Link>
          </li>
        </ul>
      </nav>
    );
  }
}
export default connect(null, { cancelSearch })(NavBar);
jsx

Reducer

reducer.js

1
2
3
4
5
6
7
8
9
10
import { API_SUCCESS } from "./actions/api";

export default (state = [], action) => {
  switch (action.type) {
    case API_SUCCESS:
      return action.payload;
    default:
      return state;
  }
};
js

Applying Middleware to the Store

The applyMiddleware() method of Redux combines the middlewares. I have created a higher-order function that will first create the store object and then pass it to the applyMiddleware() method.

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ..

import { createStore, applyMiddleware } from "redux";

import { appMiddleware } from "./middlewares/app";
import { apiMiddleware } from "./middlewares/core";

const createStoreWithMiddleware = applyMiddleware(
  appMiddleware,
  apiMiddleware
)(createStore);

const store = createStoreWithMiddleware(reducer);

// ...
jsx

Conclusion

In this guide, we learned how to could handle HTTP requests using custom middlewares. It's that simple—there is no need to add other middlewares like redux-thunk. Writing custom middlewares will help you write code specific to your use case and reduce the overall bundle size of the app. Aborting a request will always keep you on the safe side, as you won't have to troubleshoot why your app is behaving so weirdly on a decade-old mobile handset. Trust me, and it's not a pleasant experience to troubleshoot something that you don't understand fully.

That's it from this guide. Hope you are going to rock your next Redux project by writing middleware functions. If you have any queries regarding this topic, feel free to contact me at CodeAlphabet.

7