Sunday, May 19, 2024
 Popular · Latest · Hot · Upcoming
104
rated 0 times [  111] [ 7]  / answers: 1 / hits: 12710  / 3 Years ago, wed, december 1, 2021, 12:00:00

I want to cancel a promise in my React application using the AbortController and unfortunately the abort event is not recognized so that I cannot react to it.


My setup looks like this:


WrapperComponent.tsx: Here I'm creating the AbortController and pass the signal to my method calculateSomeStuff that returns a Promise. The controller I'm passing to my Table component as a prop.


export const WrapperComponent = () => {
const controller = new AbortController();
const signal = abortController.signal;

// This function gets called in my useEffect
// I'm passing signal to the method calculateSomeStuff
const doSomeStuff = (file: any): void => {
calculateSomeStuff(signal, file)
.then((hash) => {
// do some stuff
})
.catch((error) => {
// throw error
});
};

return (<Table controller={controller} />)
}

The calculateSomeStuff method looks like this:


export const calculateSomeStuff = async (signal, file): Promise<any> => {
if (signal.aborted) {
console.log('signal.aborted', signal.aborted);
return Promise.reject(new DOMException('Aborted', 'AbortError'));
}

for (let i = 0; i <= 10; i++) {
// do some stuff
}

const secret = 'ojefbgwovwevwrf';

return new Promise((resolve, reject) => {
console.log('Promise Started');
resolve(secret);

signal.addEventListener('abort', () => {
console.log('Aborted');
reject(new DOMException('Aborted', 'AbortError'));
});
});
};

Within my Table component I call the abort() method like this:


export const Table = ({controller}) => {
const handleAbort = ( fileName: string) => {
controller.abort();
};

return (
<Button
onClick={() => handleAbort()}
/>
);
}

What am I doing wrong here? My console.logs are not visible and the signal is never set to true after calling the handleAbort handler.


More From » reactjs

 Answers
13

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.


Async code that doesn't actually await doesn't have any benefits. It will not pause to unblock the thread.


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


[#626] Thursday, November 25, 2021, 3 Years  [reply] [flag answer]
Only authorized users can answer the question. Please sign in first, or register a free account.
mckaylab

Total Points: 311
Total Questions: 120
Total Answers: 93

Location: Montenegro
Member since Thu, Jun 16, 2022
2 Years ago
;