You can make use of Abort Signal, to abort all promises,
const controller = new AbortController();
const fetch = new Promise((resolve, reject) => {
// fetch data here
controller.signal.addEventListener('abort', () => reject());
});
controller.abort(); // promise is cancelled
Translating to your use-case:
const controller = new AbortController();
useEffect(() => {
const fetchAllData = async () => {
const resourceOne = new Promise((resolve, reject) => {
// fetching data from resource one and changing some states
controller.signal.addEventListener('abort', () => reject());
})
const resourceTwo = new Promise((resolve, reject) => {
// fetching data from resource two and changing some states
controller.signal.addEventListener('abort', () => reject());
})
const resourceThree = new Promise((resolve, reject) => {
// fetching data from resource three and changing some states
controller.signal.addEventListener('abort', () => reject());
})
await Promise.all([resourceOne, resourceTwo, resourceThree])
.then(() => {
// changing some states
})
}
fetchAllData();
return () => {
controller.abort(); // all promise are cancelled where 'abort' listener is specified
}
},[])
And call controller.abort() to cancel one or more number of promises where you included the event listener when the component unmounts.
When abort() is called, the promise rejects with an AbortError, so you can listen to that and handle aborted rejects differently.
Take this code as an example:
const [results, setResults] = useState([]);
useEffect(() => {
mySearchAPI(input).then(results => setResults(results));
},[input]);So this runs every time "input" changes. If the promise I'm calling is seriously inconsistent this could happen:
> call search api with the input set to 'A'; > call search api with the input set to 'AB'; > promise with input 'AB' finishes first because the api is inconsistent > "results" state is set to the "AB" query results > promise with input 'A' finishes > "results" state is set to the "A" query results
The result is an unresponsive UI.
I'm already debouncing the input, but that doesn't fix some more extreme cases. Is there an elegant way to "cancel" previously running promises when the effect is re-run?
I don't think the search provider I'm using has an api to cancel requests (like the one axios has), but anyway I don't think I'd want to rely on that.
reactjs - Correct way to cleanup useEffect with Promise - Stack Overflow
reactjs - Clean up async function in an useEffect React hook - Stack Overflow
How to Cancel subscription in async promise to avoid memory leak in Reactjs
reactjs - To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function - Stack Overflow
You can make use of Abort Signal, to abort all promises,
const controller = new AbortController();
const fetch = new Promise((resolve, reject) => {
// fetch data here
controller.signal.addEventListener('abort', () => reject());
});
controller.abort(); // promise is cancelled
Translating to your use-case:
const controller = new AbortController();
useEffect(() => {
const fetchAllData = async () => {
const resourceOne = new Promise((resolve, reject) => {
// fetching data from resource one and changing some states
controller.signal.addEventListener('abort', () => reject());
})
const resourceTwo = new Promise((resolve, reject) => {
// fetching data from resource two and changing some states
controller.signal.addEventListener('abort', () => reject());
})
const resourceThree = new Promise((resolve, reject) => {
// fetching data from resource three and changing some states
controller.signal.addEventListener('abort', () => reject());
})
await Promise.all([resourceOne, resourceTwo, resourceThree])
.then(() => {
// changing some states
})
}
fetchAllData();
return () => {
controller.abort(); // all promise are cancelled where 'abort' listener is specified
}
},[])
And call controller.abort() to cancel one or more number of promises where you included the event listener when the component unmounts.
When abort() is called, the promise rejects with an AbortError, so you can listen to that and handle aborted rejects differently.
You can use a boolean and toggle it in the cleanup function (the function that is returned in the callback passed to useEffect)
useEffect(() => {
let shouldUpdate = true;
const fetchAllData = () => {
const resourceOne = new Promise((resolve) => {
// fetching data from resource one and changing some states
})
const resourceTwo = new Promise((resolve) => {
// fetching data from resource two and changing some states
})
const resourceThree = new Promise((resolve) => {
// fetching data from resource three and changing some states
})
Promise.all([resourceOne, resourceTwo, resourceThree])
.then(() => {
if(shouldUpdate) {
// update state
}
})
}
fetchAllData()
return () => {
shouldUpdate = false;
}
},[])
If the component is unmounted the cleanup function will be called and shouldUpdate will change to false. When the promises resolve, the state will not update as shouldUpdate is no longer true.
This tutorial which will help you to resolve your issue.
Quick example: with Promises
function BananaComponent() {
const [bananas, setBananas] = React.useState([])
React.useEffect(() => {
let isSubscribed = true
fetchBananas().then( bananas => {
if (isSubscribed) {
setBananas(bananas)
}
})
return () => isSubscribed = false
}, []);
return (
<ul>
{bananas.map(banana => <li>{banana}</li>)}
</ul>
)
}
Quick example: with async/await (Not the best one but that should work with an anonymous function)
function BananaComponent() {
const [bananas, setBananas] = React.useState([])
React.useEffect(() => {
let isSubscribed = true
async () => {
const bananas = await fetchBananas();
if (isSubscribed) {
setBananas(bananas)
}
})();
return () => isSubscribed = false
}, []);
return (
<ul>
{bananas.map(banana => <li>{banana}</li>)}
</ul>
)
}
First issue
If your useEffect() fetches data acynchronously then it would be a very good idea to have a cleanup function to cancel the non-completed fetch. Otherwise what could happen is like that: fetch takes longer than expected, meantime the component is re-rendered for whatever reason. Maybe because its parent is re-rendered. The cleanup of useEffect runs before re-render and the useEffect itself runs after re-render. To avoid having another fetch inflight it's better to cancel the previous one. Sample code:
const [data, setData] = useState();
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const apiData = await fetch("https://<yourdomain>/<api-path>",
{ signal: controller.signal });
setData(apiData);
} catch (err) {
if (err.name === 'AbortError') {
console.log("Request aborted");
return;
}
}
};
fetchData();
return () => {
controller.abort();
}
});
Second issue
This code
return async dispatch => {
will not work because neither dispatch nor Redux store support async actions. The most flexible and powerful way to handle this issue is to use middleware like redux-saga. The middleware lets you:
- dispatch 'usual' sync actions to Redux store.
- intercept those sync actions and in response make one or several async calls doing whatever you want.
- wait until async call(s) finish and in response dispatch one or several sync actions to Redux store, either the original ones which you intercepted or different ones.
I think the problem is caused by dismount before async call finished.
const useAsync = () => {
const [data, setData] = useState(null)
const mountedRef = useRef(true)
const execute = useCallback(() => {
setLoading(true)
return asyncFunc()
.then(res => {
if (!mountedRef.current) return null
setData(res)
return res
})
}, [])
useEffect(() => {
return () => {
mountedRef.current = false
}
}, [])
}
mountedRef is used here to indicate if the component is still mounted. And if so, continue the async call to update component state, otherwise, skip it.
This should be the main reason to not end up with a memory leak (access cleaned up memory) issue.
Demo
https://codepen.io/windmaomao/pen/jOLaOxO , fetch with useAsync
https://codepen.io/windmaomao/pen/GRvOgoa , manual fetch with useAsync
Update
The above answer leads to the following component that we use inside our team.
/**
* A hook to fetch async data.
* @class useAsync
* @borrows useAsyncObject
* @param {object} _ props
* @param {async} _.asyncFunc Promise like async function
* @param {bool} _.immediate=false Invoke the function immediately
* @param {object} _.funcParams Function initial parameters
* @param {object} _.initialData Initial data
* @returns {useAsyncObject} Async object
* @example
* const { execute, loading, data, error } = useAsync({
* asyncFunc: async () => { return 'data' },
* immediate: false,
* funcParams: { data: '1' },
* initialData: 'Hello'
* })
*/
const useAsync = (props = initialProps) => {
const {
asyncFunc, immediate, funcParams, initialData
} = {
...initialProps,
...props
}
const [loading, setLoading] = useState(immediate)
const [data, setData] = useState(initialData)
const [error, setError] = useState(null)
const mountedRef = useRef(true)
const execute = useCallback(params => {
setLoading(true)
return asyncFunc({ ...funcParams, ...params })
.then(res => {
if (!mountedRef.current) return null
setData(res)
setError(null)
setLoading(false)
return res
})
.catch(err => {
if (!mountedRef.current) return null
setError(err)
setLoading(false)
throw err
})
}, [asyncFunc, funcParams])
useEffect(() => {
if (immediate) {
execute(funcParams)
}
return () => {
mountedRef.current = false
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return {
execute,
loading,
data,
error
}
}
Update 2022
This approach has been adopted in the book https://www.amazon.com/Designing-React-Hooks-Right-Way/dp/1803235950 where this topic has been mentioned in useRef and custom hooks chapters, and more examples are provided there.
Update 2023
Google AI response: React 18 no longer shows a warning about memory leaks when you try to update the state of a component that has been removed/unmounted. This is because React 18 has improved its memory management so that it is less likely to cause memory leaks. However, there are still some cases where it is possible to cause a memory leak in React 18. One way to do this is to create an event listener that is not removed when the component unmounts. Another way to cause a memory leak is to use a ref that is not cleaned up when the component unmounts. If you are experiencing memory leaks in your React 18 application, you can use the React DevTools to track down the source of the leak. The React DevTools will show you which components are using the most memory and which components are not being unmounted properly. Once you have identified the source of the leak, you can fix it by removing the event listener or cleaning up the ref.
I created a pen to demo it but failed: https://codepen.io/windmaomao/pen/XWyLrOa?editors=1011
useEffect will try to keep communications with your data-fetching procedure even while the component has unmounted. Since this is an anti-pattern and exposes your application to memory leakage, cancelling the subscription to useEffect optimizes your app.
In the simple implementation example below, you'd use a flag (isSubscribed) to determine when to cancel your subscription. At the end of the effect, you'd make a call to clean up.
export const useUserData = () => {
const initialState = {
user: {},
error: null
}
const [state, setState] = useState(initialState);
useEffect(() => {
// clean up controller
let isSubscribed = true;
// Try to communicate with sever API
fetch(SERVER_URI)
.then(response => response.json())
.then(data => isSubscribed ? setState(prevState => ({
...prevState, user: data
})) : null)
.catch(error => {
if (isSubscribed) {
setState(prevState => ({
...prevState,
error
}));
}
})
// cancel subscription to useEffect
return () => (isSubscribed = false)
}, []);
return state
}
You can read up more from this blog juliangaramendy
When you fire a Promise it might take a few seconds before it resolves and by that time user might have navigated to another place in your app. So when Promise resolves setState is executed on unmounted component and you get an error - just like in your case. This may also cause memory leaks.
That's why it is best to move some of your asynchronous logic out of components.
Otherwise, you will need to somehow cancel your Promise. Alternatively - as a last resort technique (it's an antipattern) - you can keep a variable to check whether the component is still mounted:
componentDidMount(){
this.mounted = true;
this.props.fetchData().then((response) => {
if(this.mounted) {
this.setState({ data: response })
}
})
}
componentWillUnmount(){
this.mounted = false;
}
I will stress that again - this is an antipattern but may be sufficient in your case (just like they did with Formik implementation).
A similar discussion on GitHub
EDIT:
This is probably how would I solve the same problem (having nothing but React) with Hooks:
OPTION A:
import React, { useState, useEffect } from "react";
export default function Page() {
const value = usePromise("https://something.com/api/");
return (
<p>{value ? value : "fetching data..."}</p>
);
}
function usePromise(url) {
const [value, setState] = useState(null);
useEffect(() => {
let isMounted = true; // track whether component is mounted
request.get(url)
.then(result => {
if (isMounted) {
setState(result);
}
});
return () => {
// clean up
isMounted = false;
};
}, []); // only on "didMount"
return value;
}
OPTION B: Alternatively with useRef which behaves like a static property of a class which means it doesn't make component rerender when it's value changes:
function usePromise2(url) {
const isMounted = React.useRef(true)
const [value, setState] = useState(null);
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
useEffect(() => {
request.get(url)
.then(result => {
if (isMounted.current) {
setState(result);
}
});
}, []);
return value;
}
// or extract it to custom hook:
function useIsMounted() {
const isMounted = React.useRef(true)
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
return isMounted; // returning "isMounted.current" wouldn't work because we would return unmutable primitive
}
Example: https://codesandbox.io/s/86n1wq2z8
The friendly people at React recommend wrapping your fetch calls/promises in a cancelable promise. While there is no recommendation in that documentation to keep the code separate from the class or function with the fetch, this seems advisable because other classes and functions are likely to need this functionality, code duplication is an anti-pattern, and regardless the lingering code should be disposed of or canceled in componentWillUnmount(). As per React, you can call cancel() on the wrapped promise in componentWillUnmount to avoid setting state on an unmounted component.
The provided code would look something like these code snippets if we use React as a guide:
const makeCancelable = (promise) => {
let hasCanceled_ = false;
const wrappedPromise = new Promise((resolve, reject) => {
promise.then(
val => hasCanceled_ ? reject({isCanceled: true}) : resolve(val),
error => hasCanceled_ ? reject({isCanceled: true}) : reject(error)
);
});
return {
promise: wrappedPromise,
cancel() {
hasCanceled_ = true;
},
};
};
const cancelablePromise = makeCancelable(fetch('LINK HERE'));
constructor(props){
super(props);
this.state = {
isLoading: true,
dataSource: [{
name: 'loading...',
id: 'loading',
}]
}
}
componentDidMount(){
cancelablePromise.
.then((response) => response.json())
.then((responseJson) => {
this.setState({
isLoading: false,
dataSource: responseJson,
}, () => {
});
})
.catch((error) =>{
console.error(error);
});
}
componentWillUnmount() {
cancelablePromise.cancel();
}
---- EDIT ----
I have found the given answer may not be quite correct by following the issue on GitHub. Here is one version that I use which works for my purposes:
export const makeCancelableFunction = (fn) => {
let hasCanceled = false;
return {
promise: (val) => new Promise((resolve, reject) => {
if (hasCanceled) {
fn = null;
} else {
fn(val);
resolve(val);
}
}),
cancel() {
hasCanceled = true;
}
};
};
The idea was to help the garbage collector free up memory by making the function or whatever you use null.
Based off your code, there are a few corrections to make:
Don't return new Promise() inside an async function
You use new Promise if you're taking something event-based but naturally asynchronous, and wrap it into a Promise. Examples:
- setTimeout
- Web Worker messages
- FileReader events
But in an async function, your return value will already be converted to a promise. Rejections will automatically be converted to exceptions you can catch with try/catch. Example:
async function MyAsyncFunction(): Promise<number> {
try {
const value1 = await functionThatReturnsPromise(); // unwraps promise
const value2 = await anotherPromiseReturner(); // unwraps promise
if (problem)
throw new Error('I throw, caller gets a promise that is eventually rejected')
return value1 + value2; // I return a value, caller gets a promise that is eventually resolved
} catch(e) {
// rejected promise and other errors caught here
console.error(e);
throw e; // rethrow to caller
}
}
The caller will get a promise right away, but it won't be resolved until the code hits the return statement or a throw.
What if you have work that needs to be wrapped with a Promise constructor, and you want to do it from an async function? Put the Promise constructor in a separate, non-async function. Then await the non-async function from the async function.
function wrapSomeApi() {
return new Promise(...);
}
async function myAsyncFunction() {
await wrapSomeApi();
}
When using new Promise(...), the promise must be returned before the work is done
Your code should roughly follow this pattern:
function MyAsyncWrapper() {
return new Promise((resolve, reject) => {
const workDoer = new WorkDoer();
workDoer.on('done', result => resolve(result));
workDoer.on('error', error => reject(error));
// exits right away while work completes in background
})
}
You almost never want to use Promise.resolve(value) or Promise.reject(error). Those are only for cases where you have an interface that needs a promise but you already have the value.
AbortController is for fetch only
The folks that run TC39 have been trying to figure out cancellation for a while, but right now there's no official cancellation API.
AbortController is accepted by fetch for cancelling HTTP requests, and that is useful. But it's not meant for cancelling regular old work.
Luckily, you can do it yourself. Everything with async/await is a co-routine, there's no pre-emptive multitasking where you can abort a thread or force a rejection. Instead, you can create a simple token object and pass it to your long running async function:
const token = { cancelled: false };
await doLongRunningTask(params, token);
To do the cancellation, just change the value of cancelled.
someElement.on('click', () => token.cancelled = true);
Long running work usually involves some kind of loop. Just check the token in the loop, and exit the loop if it's cancelled
async function doLongRunningTask(params: string, token: { cancelled: boolean }) {
for (const task of workToDo()) {
if (token.cancelled)
throw new Error('task got cancelled');
await task.doStep();
}
}
Since you're using react, you need token to be the same reference between renders. So, you can use the useRef hook for this:
function useCancelToken() {
const token = useRef({ cancelled: false });
const cancel = () => token.current.cancelled = true;
return [token.current, cancel];
}
const [token, cancel] = useCancelToken();
// ...
return <>
<button onClick={ () => doLongRunningTask(token) }>Start work</button>
<button onClick={ () => cancel() }>Cancel</button>
</>;
hash-wasm is only semi-async
You mentioned you were using hash-wasm. This library looks async, as all its APIs return promises. But in reality, it's only await-ing on the WASM loader. That gets cached after the first run, and after that all the calculations are synchronous.
Even if it is wrapped in an async function or in a function returning a Promise, code must yield the thread to act concurrently, which hash-wasm does not appear to do in its main computation loop.
So how can you let your code breath if you've got CPU intensive code like what hash-wasm uses? You can do your work in increments, and schedule those increments with setTimeout:
for (const step of stepsToDo) {
if (token.cancelled)
throw new Error('task got cancelled');
// schedule the step to run ASAP, but let other events process first
await new Promise(resolve => setTimeout(resolve, 0));
const chunk = await loadChunk();
updateHash(chunk);
}
(Note that I'm using a Promise constructor here, but awaiting immediately instead of returning it)
The technique above will run slower than just doing the task. But by yielding the thread, stuff like React updates can execute without an awkward hang.
If you really need performance, check out Web Workers, which let you do CPU-heavy work off-thread so it doesn't block the main thread. Libraries like workerize can help you convert async functions to run in a worker.
That's everything I have for now, I'm sorry for writing a novel
I can suggest my library (use-async-effect2) for managing the cancellation of asynchronous tasks/promises. Here is a simple demo with nested async function cancellation:
import React, { useState } from "react";
import { useAsyncCallback } from "use-async-effect2";
import { CPromise } from "c-promise2";
// just for testing
const factorialAsync = CPromise.promisify(function* (n) {
console.log(`factorialAsync::${n}`);
yield CPromise.delay(500);
return n != 1 ? n * (yield factorialAsync(n - 1)) : 1;
});
function TestComponent({ url, timeout }) {
const [text, setText] = useState("");
const myTask = useAsyncCallback(
function* (n) {
for (let i = 0; i <= 5; i++) {
setText(`Working...${i}`);
yield CPromise.delay(500);
}
setText(`Calculating Factorial of ${n}`);
const factorial = yield factorialAsync(n);
setText(`Done! Factorial=${factorial}`);
},
{ cancelPrevious: true }
);
return (
<div>
<div>{text}</div>
<button onClick={() => myTask(15)}>
Run task
</button>
<button onClick={myTask.cancel}>
Cancel task
</button>
</div>
);
}