React - custom async useEffect hook
In this article, we would like to show you how to create your own async useEffect hook function in React.
By default, React doesn't provide that kind of function, but there is a simple way how to create your own one.
Quick solution:
const useAsyncEffect = (callback, dependences) => {
React.useEffect(() => {
let invalid = false;
let cleanup = null;
try {
const promise = callback(() => invalid);
if (promise instanceof Promise) {
promise
.then(result => {
if (invalid) {
result?.();
} else {
cleanup = result;
}
})
.catch(console.error);
} else {
cleanup = promise; // in this case promise variable is just notmal cleanup function
}
} finally {
return () => {
invalid = true;
cleanup?.();
};
}
}, dependences);
};
// Usage example:
useAsyncEffect(async (isInvalid) => {
console.log('MyComponent mounted...');
//TODO: some async operations with await keyword here...
if (!isInvalid()) {
//TODO: we can update component state here if still is mounted...
}
return () => {
// Unmounted state handled because of empty array as dependencies used with useAsyncEffect().
// To understand this case better, go to:
// https://dirask.com/posts/React-onMount-and-onUnmount-component-1AqNz1
console.log('MyComponent unmounted...');
};
}, []);
The main advantage of that kind function is the possibility to use await keyword when we do AJAX or other async operations with useEffect, that makes code more readable. In the below approach, we can use easily isInvalid() function that checks if the component state should be modified still because of current dependencies - after async AJAX request is done current conditions can be incorrect to bind result (different dependency values).
useAsyncEffect with AJAX example
In this section, we would like to show you how to use useAsyncEffect with fetch function (with AJAX requests).
Note: in below example we can check
isInvalid()status always after any method withawaitkeyword is called to finishuseEffectlogic execution in proper way, but most important is to do not change state on unmounted component what we do.
// ONLINE-RUNNER:browser;
//Note: Uncomment import lines during working with JSX Compiler.
// import React from "react";
// import ReactDOM from "react-dom";
const useAsyncEffect = (callback, dependences) => {
React.useEffect(() => {
let invalid = false;
let cleanup = null;
try {
const promise = callback(() => invalid);
if (promise instanceof Promise) {
promise
.then(result => {
if (invalid) {
result?.();
} else {
cleanup = result;
}
})
.catch(console.error);
} else {
cleanup = promise; // in this case promise variable is just notmal cleanup function
}
} finally {
return () => {
invalid = true;
cleanup?.();
};
}
}, dependences);
};
// Usage example:
const MyComponent = ({requestText}) => {
const [response, setResponse] = React.useState();
useAsyncEffect(async (isInvalid) => {
console.log('MyComponent mounted...');
try {
const safeRequestText = encodeURIComponent(requestText);
const response = await fetch(`/examples/echo?text=${safeRequestText}`);
const responseText = await response.text();
if (!isInvalid()) {
setResponse(`Response: ${responseText}`);
}
} catch (error) {
if (!isInvalid()) {
setResponse('Request error!');
}
}
return () => {
console.log('MyComponent unmounted...');
};
}, []);
return (
<div>{response}</div>
);
};
const App = () => {
const [visible, setVisible] = React.useState(false);
const createHandleClick = (timeout) => {
return () => {
setVisible(true);
setTimeout(() => setVisible(false), timeout);
};
};
return (
<div>
<div>
<span>Mount MyComponent and later unmount </span>
{' '}
<button disabled={visible} onClick={createHandleClick()}>
immediately
</button>
{' or '}
<button disabled={visible} onClick={createHandleClick(1000)}>
after 1s
</button>
</div>
{visible && <MyComponent requestText="Hi there!" />}
</div>
);
};
const root = document.querySelector('#root');
ReactDOM.render(<App />, root);
useAsyncEffect as timer example
async useEffect can be easy way used to write own timer logic. In the below example we used sleep() method that slows down useAsyncEffect function for 1 second.
// ONLINE-RUNNER:browser;
//Note: Uncomment import lines during working with JSX Compiler.
// import React from "react";
// import ReactDOM from "react-dom";
const useAsyncEffect = (callback, dependences) => {
React.useEffect(() => {
let invalid = false;
let cleanup = null;
try {
const promise = callback(() => invalid);
if (promise instanceof Promise) {
promise
.then(result => {
if (invalid) {
result?.();
} else {
cleanup = result;
}
})
.catch(console.error);
} else {
cleanup = promise; // in this case promise variable is just notmal cleanup function
}
} finally {
return () => {
invalid = true;
cleanup?.();
};
}
}, dependences);
};
// Usage example:
const sleep = (time) => new Promise(resolve => setTimeout(resolve, time));
const App = () => {
const [counter, setCounter] = React.useState(0);
useAsyncEffect(async () => {
await sleep(1000);
setCounter(counter + 1);
}, [counter]);
return (
<div>{counter}</div>
);
};
const root = document.querySelector('#root');
ReactDOM.render(<App />, root);