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 withawait
keyword is called to finishuseEffect
logic 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);