Automatic cancellation of async code inside React components with useAsyncEffect & useAsyncCallback hooks
Tags: javascript, react, node, webdev
Published: 2021-01-06 13:59:59

I've just made the experimental `useAsyncEffect` and `useAsyncCallback` ([use-async-effect2](https://www.npmjs.com/package/use-async-effect2) npm package) hooks that can automatically cancel internal async routines when component unmounting. Also, the cancel action can be raised by the user. Correctly canceling asynchronous routines is important to avoid the well-known React warning:
`Warning: Can't perform a React state update on an unmounted component. This is an no-op, but it indicates a memory leak in your application.`
To make it work, generators are used as a replacement of async functions and basically, you just need to use `yield` keyword instead of `await`. The cancellable promise is provided by another my project- CPromise ([cpromise2](https://www.npmjs.com/package/c-promise2)).
- `useAsyncEffect` minimal example (**note, the network request will be aborted and not just ignored if you hit the `remount` button while fetching**):
```javascript
import React, { useState } from "react";
import { useAsyncEffect } from "use-async-effect2";
import cpFetch from "cp-fetch"; //cancellable c-promise fetch wrapper

export default function TestComponent(props) {
  const [text, setText] = useState("");

  useAsyncEffect(
    function* () {
      setText("fetching...");
      const response = yield cpFetch(props.url);
      const json = yield response.json();
      setText(`Success: ${JSON.stringify(json)}`);
    },
    [props.url]
  );

  return <div>{text}</div>;
}
```
{% codesandbox friendly-murdock-wxq8u %}
- With error handling:
```javascript
import React, { useState } from "react";
import { useAsyncEffect, E_REASON_UNMOUNTED } from "use-async-effect2";
import { CanceledError } from "c-promise2";
import cpFetch from "cp-fetch";

export default function TestComponent(props) {
  const [text, setText] = useState("");

  const cancel = useAsyncEffect(
    function* ({ onCancel }) {
      console.log("mount");

      this.timeout(5000);

      onCancel(() => console.log("scope canceled"));

      try {
        setText("fetching...");
        const response = yield cpFetch(props.url);
        const json = yield response.json();
        setText(`Success: ${JSON.stringify(json)}`);
      } catch (err) {
        CanceledError.rethrow(err, E_REASON_UNMOUNTED); //passthrough
        setText(`Failed: ${err}`);
      }

      return () => {
        console.log("unmount");
      };
    },
    [props.url]
  );

  return (
    <div className="component">
      <div className="caption">useAsyncEffect demo:</div>
      <div>{text}</div>
      <button onClick={cancel}>Abort</button>
    </div>
  );
}
```
{% codesandbox async-effect-demo1-vho29 %}
- `useAsyncCallback` demo:
```javascript
import React from "react";
import { useState } from "react";
import { useAsyncCallback } from "use-async-effect2";
import { CPromise } from "c-promise2";

export default function TestComponent() {
  const [text, setText] = useState("");

  const asyncRoutine = useAsyncCallback(function* (v) {
    setText(`Stage1`);
    yield CPromise.delay(1000);
    setText(`Stage2`);
    yield CPromise.delay(1000);
    setText(`Stage3`);
    yield CPromise.delay(1000);
    setText(`Done`);
    return v;
  });

  const onClick = () => {
    asyncRoutine(123).then(
      (value) => {
        console.log(`Result: ${value}`);
      },
      (err) => console.warn(err)
    );
  };

  return (
    <div className="component">
      <div className="caption">useAsyncCallback demo:</div>
      <button onClick={onClick}>Run async job</button>
      <div>{text}</div>
    </div>
  );
}
```
{% codesandbox use-async-callback-bzpek %}
Any feedback is appreciated 😊