DEV Community

Cover image for Automatic cancellation of async code inside React components with useAsyncEffect & useAsyncCallback hooks
Dmitriy Mozgovoy
Dmitriy Mozgovoy

Posted on • Edited on

Automatic cancellation of async code inside React components with useAsyncEffect & useAsyncCallback hooks

I've just made the experimental useAsyncEffect and useAsyncCallback (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).

  • useAsyncEffect minimal example (note, the network request will be aborted and not just ignored if you hit the remount button while fetching):
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>;
}
Enter fullscreen mode Exit fullscreen mode

  • With error handling:
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>
  );
}
Enter fullscreen mode Exit fullscreen mode

  • useAsyncCallback demo:
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>
  );
}
Enter fullscreen mode Exit fullscreen mode


Any feedback is appreciated 😊

Top comments (0)