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
  • 41,300 Views
  • Dec 20, 2019
  • 13 Min read
  • 41,300 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.

1create-react-app cancel-demo
console

Next, we will install the required npm modules.

1npm 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

1import React from "react";
2
3export default props => (
4  <div>
5    <p>This is the Home Page.</p>
6    <p>Go to Search Page and search some images.</p>
7  </div>
8);
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.

1import React, { Component } from "react";
2import { connect } from "react-redux";
3import { search } from "./actions/search";
4
5class Search extends Component {
6  constructor(props) {
7    super(props);
8    this.state = {
9      searchTerm: "",
10      images: []
11    };
12  }
13
14  render() {
15    return (
16      <div>
17        <input
18          value={this.state.searchTerm}
19          onChange={e => this.setState({ searchTerm: e.target.value })}
20        />
21        <button onClick={() => this.props.search(this.state.searchTerm)}>
22          Search
23        </button>
24      </div>
25    );
26  }
27}
28
29export default connect(null, {
30  search
31})(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.

1import React from "react";
2import ReactDOM from "react-dom";
3import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
4
5import { Provider } from "react-redux";
6import { createStore } from "redux";
7
8import reducer from "./reducer";
9
10import Home from "./Home";
11import Search from "./Search";
12import Navbar from "./Navbar";
13
14import { appMiddleware } from "./middlewares/app";
15import { apiMiddleware } from "./middlewares/core";
16
17const store = createStore(reducer);
18
19function App() {
20  return (
21    <Provider store={store}>
22      <Router>
23        <div>
24          <Navbar />
25          <Switch>
26            <Route path="/search">
27              <Search />
28            </Route>
29            <Route path="/" default>
30              <Home />
31            </Route>
32          </Switch>
33        </div>
34      </Router>
35    </Provider>
36  );
37}
38
39const rootElement = document.getElementById("root");
40ReactDOM.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// action types
2export const API_REQUEST = "API_REQUEST";
3export const API_SUCCESS = "API_SUCCESS";
4export const API_ERROR = "API_ERROR";
5export const CANCEL_API_REQUEST = "CANCEL_API_REQUEST";
6
7// action creators
8export const apiRequest = ({ url, method }) => {
9  return {
10    type: API_REQUEST,
11    meta: { url, method }
12  };
13};
14
15export const cancelApiRequest = () => {
16  return {
17    type: CANCEL_API_REQUEST
18  };
19};
20
21export const apiSuccess = ({ response }) => ({
22  type: API_SUCCESS,
23  payload: response
24});
25
26export const apiError = ({ error }) => ({
27  type: API_ERROR,
28  payload: error
29});
js

ui.js

1export const SET_LOADER = "SET_LOADER";
2
3export const setLoader = ({ state }) => ({
4  type: SET_LOADER,
5  payload: state
6});
js

search.js

1export const SEARCH = "SEARCH";
2export const CANCEL_SEARCH = "CANCEL_SEARCH";
3
4export const search = term => {
5  return {
6    type: SEARCH,
7    payload: term
8  };
9};
10
11export const cancelSearch = () => {
12  return {
13    type: CANCEL_SEARCH
14  };
15};
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.

1const middlewareFunction = action => {
2  // ...
3};
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.

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

Simplifiying the above snippet we get,

1const middlewareFunction = next => action => {
2  // ...
3};
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.

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

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

1const middlewareFunction = store => next => action => {
2  next(action);
3};
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.

1const CancelToken = axios.CancelToken;
2const source = CancelToken.source();
3
4// pass the source to axios call
5axios(method, url, { cancelToken: source.token });
js

core.js

1import axios from "axios";
2import {
3  API_REQUEST,
4  apiError,
5  apiSuccess,
6  CANCEL_API_REQUEST
7} from "../actions/api";
8
9const CancelToken = axios.CancelToken;
10const source = CancelToken.source();
11
12export const apiMiddleware = ({ dispatch }) => next => action => {
13  next(action);
14
15  if (action.type === API_REQUEST) {
16    const { url, method } = action.meta;
17    axios({
18      method,
19      url,
20      cancelToken: source.token
21    })
22      .then(({ data }) => dispatch(apiSuccess({ response: data })))
23      .catch(error => {
24        console.log(error);
25        if (axios.isCancel(error)) {
26          console.log(error.message);
27        } else {
28          dispatch(apiError({ error: error.response.data }));
29        }
30      });
31  } else if (action.type === CANCEL_API_REQUEST) {
32    source.cancel("Operation canceled by the user.");
33    console.log("REQUEST CANCELLED!!!");
34  }
35};
js

app.js

1import { SEARCH, CANCEL_SEARCH } from "../actions/search";
2import { apiRequest, cancelApiRequest } from "../actions/api";
3import { setLoader } from "../actions/ui";
4
5const PIXABAY_KEY = "14545036-912e59631b7d8e4d4ebbffc6a";
6
7const PIXABAY_URL = `https://pixabay.com/api/?key=${PIXABAY_KEY}`;
8
9export const appMiddleware = () => next => action => {
10  next(action);
11  switch (action.type) {
12    case SEARCH: {
13      next(
14        apiRequest({
15          url: `${PIXABAY_URL}&q=${action.payload}`,
16          method: "GET"
17        })
18      );
19      next(setLoader({ state: true }));
20      break;
21    }
22    case CANCEL_SEARCH: {
23      next(cancelApiRequest());
24      next(setLoader({ state: false }));
25      break;
26    }
27    default:
28      break;
29  }
30};
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

1import React, { Component } from "react";
2import { connect } from "react-redux";
3import { cancelSearch } from "./actions/search";
4import { Link } from "react-router-dom";
5
6class NavBar extends Component {
7  render() {
8    return (
9      <nav>
10        <ul>
11          <li onClick={() => this.props.cancelSearch()}>
12            <Link to="/">Home</Link>
13          </li>
14          <li onClick={() => this.props.cancelSearch()}>
15            <Link to="/search">Search</Link>
16          </li>
17        </ul>
18      </nav>
19    );
20  }
21}
22export default connect(null, { cancelSearch })(NavBar);
jsx

Reducer

reducer.js

1import { API_SUCCESS } from "./actions/api";
2
3export default (state = [], action) => {
4  switch (action.type) {
5    case API_SUCCESS:
6      return action.payload;
7    default:
8      return state;
9  }
10};
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
3import { createStore, applyMiddleware } from "redux";
4
5import { appMiddleware } from "./middlewares/app";
6import { apiMiddleware } from "./middlewares/core";
7
8const createStoreWithMiddleware = applyMiddleware(
9  appMiddleware,
10  apiMiddleware
11)(createStore);
12
13const store = createStoreWithMiddleware(reducer);
14
15// ...
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.