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 theremount
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>;
}
- 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>
);
}
-
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>
);
}
Any feedback is appreciated 😊
Top comments (0)