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.

Answer from PsyGik on Stack Overflow
🌐
Reddit
reddit.com › r/reactjs › how do i cancel/ignore previously running promises in useeffect?
r/reactjs on Reddit: How do I cancel/ignore previously running promises in useEffect?
May 6, 2019 -

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.

🌐
Juliangaramendy
juliangaramendy.dev › blog › use-promise-subscription
Cancelling a Promise with React.useEffect - Julian​Garamendy​.dev
April 7, 2019 - We can fix this by cancelling our request when the component unmounts. In function components, this is done in the cleanup function of useEffect. ... React.useEffect(() => { fetchBananas().then(setBananas) return () => someHowCancelFetchBananas!
Discussions

reactjs - Correct way to cleanup useEffect with Promise - Stack Overflow
204 Promise - is it possible to force cancel a promise · -2 React Link to same component, has subcomponent using the wrong version of passed state More on stackoverflow.com
🌐 stackoverflow.com
reactjs - Clean up async function in an useEffect React hook - Stack Overflow
I have the following useEffect function and trying to find the best way to clean this up when the component unmounts. I thought it would be best to follow the makeCancelable from the React docs, however, the code still executes when the promise is cancelled. More on stackoverflow.com
🌐 stackoverflow.com
How to Cancel subscription in async promise to avoid memory leak in Reactjs
Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in %s.%s, a useEffect cleanup function · I believe this occurs because the promise doesn't have a "clean-up" ... More on stackoverflow.com
🌐 stackoverflow.com
September 30, 2019
reactjs - To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function - Stack Overflow
Thanks, finally I found a way to cancel asynchronous task while they are running 2020-04-18T07:08:06.587Z+00:00 ... This solution will no longer work post-React18 when using StrictMode due to each component being mounted more than once. As a result the initial useEffect will set the mountedRef ... More on stackoverflow.com
🌐 stackoverflow.com
🌐
Medium
medium.com › @tangiblej › cancel-a-promise-inside-react-useeffect-12a101606b72
Cancel a Promise inside React useEffect | by Thomas Junghans | Medium
April 1, 2023 - I will explain how to cancel a Promise inside React’s useEffect using the AbortController API.
Top answer
1 of 2
7

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.

2 of 2
6

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.

🌐
DEV Community
dev.to › praveenkumarrr › cancellable-promises-in-react-and-why-is-it-required-5ghf
Cancellable promises in react and why is it required - DEV Community
May 27, 2021 - The main thing about the useEffect ... function that we got from the cancellablePromise. When react calls cancel function, the promise is set to cancelled (value = true)....
🌐
CodeSandbox
codesandbox.io › s › useeffect-react-hooks-cancel-promise-h6dcw
useEffect & React Hooks & cancel Promise - CodeSandbox
October 18, 2023 - How to use the custom hook. Repo: https://github.com/JulianG/hooks
Published   Oct 18, 2019
Author   xgqfrms
🌐
DEV Community
dev.to › rodw1995 › cancel-your-promises-when-a-component-unmounts-gkl
Cancel your promises when a component unmounts - DEV Community
March 21, 2020 - The promise has to be canceled when the component unmounts. ... First we need a way to check if a component is still mounted. We can do so by making use of the cleanup function in a useEffect hook.
Find elsewhere
Top answer
1 of 2
4

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>
  )
}
2 of 2
2

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.
Top answer
1 of 11
110

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

2 of 11
46

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

🌐
Shevtsov
leonid.shevtsov.me › post › always-cancel-useeffect-promises
Always cancel promises started in React.useEffect
December 11, 2021 - If you start a promise inside useEffect, make sure to “cancel” it in the cleanup handler. const [product, setProduct] = React.useState(null); React.useEffect(() => { // use a local variable to track effect state - not refs and certainly ...
🌐
Medium
rajeshnaroth.medium.com › writing-a-react-hook-to-cancel-promises-when-a-component-unmounts-526efabf251f
Writing a React hook to cancel promises when a component unmounts. | by Rajesh Naroth | Medium
January 3, 2023 - Now you can create a higher order component use it to cancel your pending promises when it unmounts. The only problem is you have to warp every container/component that needs this behavior with the HOC. A bit messy. You can see this issue and the solution in action in the codesandbox below. Open the console tab to watch the logs and React errors:
🌐
DEV Community
dev.to › elijahtrillionz › cleaning-up-async-functions-in-reacts-useeffect-hook-unsubscribing-3dkk
Cleaning up Async Functions in React's useEffect Hook (Unsubscribing) - DEV Community
December 2, 2021 - Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
🌐
Plain English
plainenglish.io › blog › how-to-cancel-fetch-and-axios-requests-in-react-useeffect-hook
How to Cancel Fetch and Axios Requests in React’s useEffect Hook
October 20, 2023 - Throughout this article, we’ve delved into the importance of cleaning up side effects, explored practical examples, and learned about tools like the AbortController and axios.CancelToken that can help us manage and cancel requests effectively. Cleanup Matters: Always consider the cleanup of side effects when making network requests in React components. The useEffect hook provides an excellent place to manage these effects.
🌐
DEV Community
dev.to › juliang › cancelling-a-promise-with-react-useeffect-3062
Cancelling a Promise with React.useEffect - DEV Community
March 5, 2021 - We can fix this by cancelling our request when the component unmounts. In function components, this is done in the cleanup function of useEffect. ... React.useEffect(() => { fetchBananas().then(setBananas) return () => someHowCancelFetchBananas!
Top answer
1 of 16
119

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

2 of 16
31

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.

🌐
GitHub
github.com › facebook › react › issues › 15006
useEffect memory leak when setting state in fetch promise · Issue #15006 · facebook/react
March 3, 2019 - const ArtistProfile = props => { const [artistData, setArtistData] = useState(null) const { getArtist, getArtistAlbums, getArtistTopTracks } = props.spotifyAPI useEffect(() => { const id = window.location.pathname.split("/").pop() const ac = new AbortController() console.log(id) Promise.all([ getArtist(id, ac), getArtistAlbums(id, ["album"],"US", 10, 0, ac), getArtistTopTracks(id, "US", ac) ]) .then(response => { setArtistData({ artist: response[0], artistAlbums: response[1], artistTopTracks: response[2] }) }) .catch(ex => console.error(ex)) return () => ac.abort() }, []) console.log(artistData) return ( <div> <ArtistProfileContainer> <AlbumContainer> {artistData ?
Author   ryansaam
🌐
DEV Community
dev.to › pallymore › clean-up-async-requests-in-useeffect-hooks-90h
Clean Up Async Requests in `useEffect` Hooks - DEV Community
October 12, 2019 - A new token is created for every new "effect". cancel / abort is called whenever the effect re-fires (e.g. when the parameters changed, or when the component unmounts), the cleanup function is called, cancelling the previous request - in your ...
Top answer
1 of 3
22

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

2 of 3
0

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>
      );
    }
🌐
DEV Community
dev.to › n1ru4l › homebrew-react-hooks-useasynceffect-or-how-to-handle-async-operations-with-useeffect-1fa8
Homebrew React Hooks: useAsyncEffect Or How to Handle Async Operations with useEffect - DEV Community
March 31, 2020 - In that case, the "newer" data will be overwritten by the "old" data once the delayed promise has resolved 😲. ... In an ideal world, we would not have to care about such things, but unfortunately, it is not possible to cancel an async function.