Skip to content

Instantly share code, notes, and snippets.

@gragland
Last active February 17, 2024 16:42
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save gragland/33b5d146891c77ceb1f3493a2428f026 to your computer and use it in GitHub Desktop.
Save gragland/33b5d146891c77ceb1f3493a2428f026 to your computer and use it in GitHub Desktop.
React Hook recipe from https://usehooks.com
import React, { useState, useEffect, useCallback } from 'react';
// Usage
function App() {
const { execute, status, value, error } = useAsync(myFunction, false);
return (
<div>
{status === 'idle' && <div>Start your journey by clicking a button</div>}
{status === 'success' && <div>{value}</div>}
{status === 'error' && <div>{error}</div>}
<button onClick={execute} disabled={status === 'pending'}>
{status !== 'pending' ? 'Click me' : 'Loading...'}
</button>
</div>
);
}
// An async function for testing our hook.
// Will be successful 50% of the time.
const myFunction = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const rnd = Math.random() * 10;
rnd <= 5
? resolve('Submitted successfully 🙌')
: reject('Oh no there was an error 😞');
}, 2000);
});
};
// Hook
const useAsync = (asyncFunction, immediate = true) => {
const [status, setStatus] = useState('idle');
const [value, setValue] = useState(null);
const [error, setError] = useState(null);
// The execute function wraps asyncFunction and
// handles setting state for pending, value, and error.
// useCallback ensures the below useEffect is not called
// on every render, but only if asyncFunction changes.
const execute = useCallback(() => {
setStatus('pending');
setValue(null);
setError(null);
return asyncFunction()
.then(response => {
setValue(response);
setStatus('success');
})
.catch(error => {
setError(error);
setStatus('error');
});
}, [asyncFunction]);
// Call execute if we want to fire it right away.
// Otherwise execute can be called later, such as
// in an onClick handler.
useEffect(() => {
if (immediate) {
execute();
}
}, [execute, immediate]);
return { execute, status, value, error };
};
@devgioele
Copy link

@xandris pointed out a very important issue here! For example, I pass a http fetch as the execute function, which then triggers other effects. In such a case, the execute function is called 3 times instead of just 1.

@xandris
Copy link

xandris commented Mar 16, 2022

@devgioele i barely remember this! i think the issue is the function returned from useCallback isn't guaranteed to be === for the same dependencies...maybe a plain ref is right right answer; something guaranteed to be stable. although i've never seen any evidence of this 'cache eviction behavior', i just remember the React team left that option open for them. your issue might also be caused by rerenders higher up the tree though? when things get real deep it's easy to miss like key attributes, or subtrees might be getting unmounted and remounted by effects... it could also be parameters to the useAsync hook itself, maybe those aren't reference stable?

@devgioele
Copy link

devgioele commented Mar 17, 2022

Thanks for the quick reply @xandris! The actual problem was that not passing a reference stable function to useAsync caused infinite calls. To avoid that this function is recreated on each render, my solution is to wrap this function first with useCallback and then passing it to useAsync.

The fact that execute was called 3 times... that was because I was actually using it in 3 different places, but didn't realize it!

@TusharShahi
Copy link

Is it ideal to use a function as a useCallback dependency? Function is created fresh in every render (unless handled).

@tamiradler
Copy link

The init value of state should be depend on immediate variable:
const [status, setStatus] = useState(() => immediate ? 'pending' : 'idle' );

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment