How to Use Component-level Cancelation in React (Promises, RxJS or Redux Query)
Tags: react, redux, rxjs
Published: 2021-05-21 16:48:08

---
title: How to Use Component-level Cancelation in React (Promises, RxJS or Redux Query)
published: true
description: Learn how to cancel effects when the components that triggered them unmount.
tags: react, redux, RxJS
cover_image: https://images.unsplash.com/photo-1520606393001-b816ded9caac?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=2100&q=80
---

## Motivation

Cancelation is first and foremost a User Experience issue. Users will perceive apps to be more responsive when resources like the network are freed up to be used by the task at hand. Cancelation is also a way to improve the experience of users on slower connections, so increases your app's reach.

The pictures below show how a search results page stops consuming network usage when it is unmounted (and how incremental rendering helps show results sooner- the topic of a future post)

With cancelation, and incremental delivery:

![](https://s3.amazonaws.com/www.deanius.com/graphql-rates-observable-unmount.gif)


No cancelation (and no incremental delivery):
![](https://s3.amazonaws.com/www.deanius.com/graphql-rates-apollo-unmount.gif)


"But how often will this matter?" is a question you might ask. That will depend on your user demographics and their connectivity, of course. But you needn't wait for complaints to arrive to build in a sensible principle. 

## Comparisons 
Cancelation wasn't always an afterthought. The first web browsers had a big red 'STOP' button so that users could cancel slow-loading pages at any time. 

![Netscape 1.0, my first browser on a mac](https://s3.amazonaws.com/www.deanius.com/netscape-stop-button.png)

As the SPA era began, about 10 years later, several things became casualties as far as User Experience (UX). Yes, "the back button broke". But also - now that request-making and asynchronous processes were no longer tied to that big red button, fire-and-forget AJAX became the norm. Browsers stopped showing a Stop button (does yours have one?), and developers stopped treating cancelation like it was critical to good UX. But it still is.

Imagine if operating systems didn't cancel child processes by default! Web development only differs by degree from that.

![Operating Systems Cancel Hierarchically](https://s3.amazonaws.com/www.deanius.com/activity-monitor-by-hierarchy-process.png)


# Code Examples With React Hooks

So how does one achieve component-level cancelation in React with hooks? We'll explore several technologies' answers to this, in the context of a real-world example. Let's say we have a component that presents a list of possible appointment times, and uses a hook called `useAppointments` to query a back-end via AJAX for whether the chosen time is available. Stripped of UI details, it would look like this:

```js
function AppointmentChooser() {
   const { beginTime, setBeginTime, isAvailable } = useAppointments();
   return <>
      <select
        onChange={(e) => setBeginTime(e.target.value)}>
        <!-- time options -->
      </select>
      <span>{beginTime} { isAvailable ? "✅" : "🚫" }
   </>
}
```

Our goal will be that any effects this component triggers will be shutdown when this appointment chooser unmounts.

# Style 1 — Vanilla JS, no cancelation

Here's how we might implement `useAppointments` without regard to cancelation:

```js
export function useAppointments() {
  const [beginTime, _setBeginTime] = useState('');
  const [isAvailable, setIsAvailable] = useState<null | 'loading' | true | false>(null);

  function setBeginTime(time: string) {
    setIsAvailable('loading');
    _setBeginTime(time);
    
    fetch(`https://httpbin.org/delay/5?t=${time}`)
      .then(({ isAvailable }) => {
        setIsAvailable(isAvailable);
      });
  };
  return { beginTime, setBeginTime, isAvailable };
}
```

The job of the hook's `setBeginTime` function is to 1) set the `beginTime` in local state, 2) set the availability to `'loading'` 3) perform the fetch, and 4) set the availability asynchronously with the result of the fetch. This function `setBeginTime` is what we will focus on as we show differing implementations. 

# Style 1.1 — Vanilla JS: Promise + AbortController

In 2018 the `AbortController` abstraction was introduced for canceling *some* Promises.  An example of a hook that uses an AbortController on each request, and cancels the fetch upon on unmount is shown below. 

```js
export function useAppointments() {
  const [beginTime, _setBeginTime] = useState('');
  const [isAvailable, setIsAvailable] = useState<null | 'loading' | true | false>(null);

  const ac = useRef<AbortController>(null);
  useEffect(() => () => ac.current.abort(), []);

  function setBeginTime(time: string) {
    setIsAvailable('loading');
    _setBeginTime(time);

    ac.current = new AbortController();
    
    fetch(`https://httpbin.org/delay/${delay}?t=${time}`, {
      signal: ac.current.signal,
    }).then(
      () => {
        setIsAvailable(true);
      },
      (ex: DOMException) => {
        if (ex.name === 'AbortError') {
  		  // not an exception
          setIsAvailable(null);
        } else { throw ex }
      }
    );
  };
  return { beginTime, setBeginTime, isAvailable };
}
```

Wow, that's a lot of code. We have to hold refs for AbortController instances. We have to use the cryptic React-hook-specific syntax to invoke cancelation, which reads, "on unmount, abort the current request".

   `useEffect(() => () => ac.current.abort(), [])` 

And then we have the exception-handling code. An aborted Promise is treated as an exception which generally will you will generally want to distinguish from a real exception like a `501` server error.

This code achieves cancelation-on-unmount with only Vanilla JS, but are there libraries we can use to have a simpler implementation?

A library exists that generalizes cancelable async processes, and will let us apply more concurrency options as transparently as cancelation. Let's see how RxJS, familiar to Angular users, approaches cancelation.

# Style 2 — RxJS + useEffect

In 2012, before Promises were even integrated into JavaScript, the ReactiveX project (now known as RxJS) introduced an inherently cancelable datatype - `Observable`. While Observable is more commonly known for its use as an asynchronous Stream, every Observable ever made is cancelable. Because of this, there will be far less code to cancel an Observable than a Promise.

The general strategy is to wrap the begin and end operations in an RxJS Observable explicitly, and return that Observable to a hook that will call `.subscribe()` on it, and call `.unsubscribe()`  on it when unmounted.

```js
import { Subscription } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import { tap } from 'rxjs/operators';

export function useAppointments() {
  const [beginTime, _setBeginTime] = useState('');
  const [isAvailable, setIsAvailable] = useState<null | 'loading' | true | false>(null);
  const process = useRef<Subscription>(null);
  useEffect(() => () => process.current.unsubscribe(), []);
  
  function setBeginTime(time: string) {
    setIsAvailable('loading');
    _setBeginTime(time);
    
    process.current = ajax
      .getJSON(`https://httpbin.org/delay/5?t=${time}`)
      .pipe(tap({ isAvailable }) => {
        setIsAvailable(isAvailable);
      }))
      .subscribe();
  };
  return { beginTime, setBeginTime, isAvailable };
}
```

Like the AbortController example, we need a `ref` to keep track of the cancelation variable. But in this case it is a Subscription, not an AbortController, and cancelation function is `unsubscribe()`, not `abort()`. `ajax.getJSON()` creates the Observable, which represents *but does not start* the AJAX call, and `.subscribe()` begins the call, and returns the Subscription which is how we cancel. The `pipe(tap(..))` construct updates the local state by calling `setIsAvailable` once a value is available from the request.

The main increase in clarity here comes from the fact that unsubscribing from an Observable (or technically, from its Subscription) is not considered an exception, so that code disappears! But we still rely on managing an extra object - the Subscription - in order to provide cancelation. Let's now make those subscription objects disappear.

# Style 3 - RxJS + useCancelableEffect

The `polyrhythm` library, introduced in 2018, lowers the learning curve and amount of code required to use RxJS. The companion library `polyrhythm-react` exports hooks for using it in a React context. Let's see how its `useCancelableEffect` function can clean up our availability-querying hook:

```js
import { useCancelableEffect } from 'polyrhythm-react';
import { ajax } from 'rxjs/ajax';
import { tap } from 'rxjs/operators';

export function useAppointments() {
  const [beginTime, _setBeginTime] = useState('');
  const [isAvailable, setIsAvailable] = useState<null | 'loading' | true | false>(null);

  const [queryAvailability] = useCancelableEffect((time: string) => {
    return ajax
      .getJSON(`https://httpbin.org/delay/5?t=${time}`)
      .pipe(tap({ isAvailable }) => {
        setIsAvailable(isAvailable);
       }));
  });
  
  function setBeginTime(time: string) {
    setIsAvailable('loading');
    _setBeginTime(time);
	queryAvailability(time);    
  };
  return { beginTime, setBeginTime, isAvailable };
}
```
This is the shortest listing yet. We provide `useCancelableEffect` a function that converts a `time` to an Observable of the AJAX query for availability and state-updating. `useCancelableEffect` returns a tuple, the first item which is a triggering function, which we name `queryAvailability`. 

After updating local state in `setBeginTime`, we call `queryAvailability` to begin the AJAX, and if at any time the component unmounts, the AJAX call will terminate! The API to getting an Observable representing an AJAX request is very similar to getting a Promise for a request, but since Observables are inherently cancelable there's no extra AbortController. This results in less code overall.

We can further generalize the process of AJAX to include the loading state - which we don't want to leave displaying "loading" if the request has canceled. Here's how we use the Observable constructor directly to incorporate teardown into cancelation:

```js

import { useCancelableEffect } from 'polyrhythm-react';
import { Observable } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import { tap } from 'rxjs/operators';

export function useAppointments() {
  const [beginTime, _setBeginTime] = useState('');
  const [isAvailable, setIsAvailable] = useState<null | 'loading' | true | false>(null);

  const [setBeginTime] = useCancelableEffect((time: string) => {
    return new Observable(observer => {
      setIsAvailable('loading');
      _setBeginTime(value);
      
      const query = ajax
        .getJSON(`https://httpbin.org/delay/5?t=${time}`)
        .pipe(tap({ isAvailable }) => {
          setIsAvailable(isAvailable);
         }));
       
      const ajax = query.subscribe({
        complete() { observer.complete(); }
      });
      
      return function teardown() {
        ajax.unsubscribe();
        setIsAvailable(null); // clear the loading state
	  }
  });
  
  return { beginTime, setBeginTime, isAvailable };
}
```
Like React's own `useEffect`, the returned value from the `new Observable` factory is a teardown function that is called upon unsubscribe. Upon teardown we should stop the AJAX, and revert the loading state to unknown aka `null`.  Upon starting the Observable, we simply need to set the loading state, and call `subscribe` to begin the AJAX. This way, a single Observable represents the entire process of AJAX, including its loading state. The argument to `subscribe` -containing `observer.complete()` - indicates that the completion of the AJAX should mark the end of the entire Observable. This just illustrates one way to compose Observable behavior with cancelation, and is not meant to be prescriptive for all cases.

This technique generalizes far beyond AJAX, and makes RxJS a real workhorse. For example, to fire-off an auto-canceling Observable that uses the browser's [Speech API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API/Using_the_Web_Speech_API):

```js
const [speakIt] = useCancelableEffect(() => new Observable(() => {
  const words = `Checking availability for ${time}`;
  speechSynthesis.speak(new SpeechSynthesisUtterance(words));

  return () => {
    window.speechSynthesis.cancel();
  };
});)
```

The ability to bundle cancelation with creation ultimately leads to optimally performant code, with fewer edge cases, and less scattering of logic. This is why RxJS is useful on the front-end, back-end, and is one of the most downloaded packages on NPM (over 2x that of React!)

# Bonus — RxJS Operators via hooks

Note that in the examples above, the code assumes that there will be no overlapping requests. But in real life, if a user isn't getting a speedy response for one appointment time, they may choose another, and bugs will result!

RxJS provides operators to deal with the concurrency issue, but in fact RxJS users' most frequent sources of confusion are is how to choose and use operators. 

For your convenience, all of the operators of RxJS have hooks in `polyrhythm-react` which let you control timing with precision (this  will be elaborated in a future post).

|  polyrhythm-react | RxJS  |
|--|--|
| useCancelableEffect/useASAPEffect | mergeMap  |
| useQueuedEffect | concatMap  |
| useRestartingEffect | switchMap  |
| useThrottledEffect | exhaustMap  |

---
# Summary

It's a best practice all over the web development stack to tie processes to the things that need them, so they tear down automatically. On the back-end, don't do work for a client who's disconnected. On the front-end, when a component that just mounted did a thing, and the user navigated to a new route that causes an unmount.

The idea of the API to the hook remaining the same, while cancelation is used internally is the best-practice way to integrate cancelable processes to React. Whether you obtain that result with Redux Query, RxJS or custom hooks is up to you. But your users and support teams want you to reduce issues that can be fixed by cancelation. And development is easier when concurrency options prevent race conditions, which canceling enables. Now you have an array of tools to help improve User Experience.

Happy canceling!


# Bonus - Redux Query useRequest

If the process we want to be cancelable is an AJAX request made with Redux Query, there is a hook that can help us. But first let's remind ourselves how a non-cancelable Redux Query hook looks.

In order to move the `isAvailable` field to be controlled by R/Q, we introduce a queryConfig `apptQueryConfig` that specifies where to locate the state in Redux, and a selector `selectApptAvailability` that finds that state. In prod code, maybe we'd move the state field of `beginTime` up to the Redux store, and out of this component as well, but for demo purposes we'll leave it.

So, for non-cancelable Redux Query we'd have:

```js
import { useDispatch, useSelector } from 'react-redux';
import { requestAsync } from 'redux-query';
import { apptQueryConfig, selectAvailability } from './appointments';

export function useAppointments() {
  const dispatch = useDispatch();
  const [beginTime, _setBeginTime] = useState('');
  const isAvailable = useSelector(selectApptAvailability);
  // state as before ...
  
  function setBeginTime(time: string) {
    setIsAvailable('loading');
    _setBeginTime(time);
    // add time to the queryConfig
    const queryConfig = apptQueryConfig(time);
    // perform the lookup
    dispatch(requestAsync());
  }  
  return { beginTime, setBeginTime, isAvailable };
}
```

After adding time to the queryConfig, it is a simple `dispatch` of a `requestAsync` action which begins the AJAX, and resolves `isAvailable`. How do we make it cancelable? There's a Redux Query hook for that: `useRequest`. With `useRequest`, we get cancelation almost 'for free'. According to R/Q docs:

> When the associated component unmounts and there is a request in-flight, then a  [`cancelQuery`](https://amplitude.github.io/redux-query/docs/redux-actions#cancelquery)  action will be dispatched which will attempt to abort the network request.

So we have the following:

```js
import { useSelector } from 'react-redux';
import { useRequest } from 'redux-query-react';
import { apptQueryConfig, selectAvailability } from './appointments';

export function useAppointments() {
  const [beginTime, _setBeginTime] = useState('');
  const isAvailable = useSelector(selectApptAvailability);
  // state as before ...
   
  const queryConfig = useMemo(() => {
    return beginTime ? apptQueryConfig(beginTime) : null;
  }, [beginTime]);
  useRequest(queryConfig);
  
  function setBeginTime(time: string) {
    setIsAvailable('loading');
    _setBeginTime(time);
    // R/Q auto-updates on changes of `beginTime`
  }

  return { beginTime, setBeginTime, isAvailable };
}
```
Nice! We have a queryConfig that is a memoized version of `beginTime`.  The queryConfig must be `null` when `beginTime` is not yet set, to accommodate the first render, since the user has not yet provided a time.  It is this config that we pass to `useRequest`, and cancelation happens behind the scene, easy peasy!

It's no surprise a popular library like Redux Query accounts for the cases of cancelation. In addition, can you see how it solves the multiple request problem? According to its docs:

> If there is a previously-issued request that is still in-flight, then a [`cancelQuery`](https://amplitude.github.io/redux-query/docs/redux-actions#cancelquery) action will be dispatched which will attempt to abort the network request.

In other words, when the user changes their mind and selects a new appointment time, all the network bandwidth goes toward the NEW appointment time- the previous one is canceled since we won't display its result anyway! This is the bedrock of good UX, and it reduces edge cases as well. We won't elaborate on concurrency here—a future post will. For now, note that cancelation isn't just useful for unmounting, but also for eliminating race conditions caused by previous requests completing *after* newer ones.