-
-
Save gragland/33b5d146891c77ceb1f3493a2428f026 to your computer and use it in GitHub Desktop.
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 }; | |
}; |
@windmaomao the relevant snippet from the sandbox:
// An async function for testing our hook. Will be successful 50% of the time.
const myFunction = () => {
return new Promise((resolve, reject) => {
resolve(() => console.log("Oh no! This is a bug!"))
});
};
@Yey007, to clarify - you're saying this will clash with functional updates.
const [state, setState] = useState({});
setState(prevState => {
return {...prevState, ...updatedValues};
});
This seems right. Nice!
@trevithj Exactly. This was a nightmare to debug, and I want to save others the pain.
@gragland I think this hook will cause re-rendering in the parent component as is calling the state more than 3 times. when clean the loading, errors, and when set the items..
here is one I made similar, this hook will setState only 1 time
/**
* Fetch Query and return result or error
* @param {Object} params
* @param {any} params.initialData - Initial items
* @param {Boolean} params.fetchOnMount - Fetch on mount
* @param {Promise} params.service - Promise to fetch
* @param {any} params.query - Query params for service
* @returns
*/
const useQueryFetch = params => {
/**
* params
*/
const { initialState = null, fetchOnMount, service, query } = params || {};
/**
* States
*/
const [instance, setInstance] = useState({
loading: true,
error: null,
firstMount: false,
items: initialState
});
/**
* Fetch data from API
*/
const onFetch = async () => {
let _instance = {
...instance
};
try {
//Fetch service
const { data } = await service(query);
//Set items
_instance.items = data;
} catch (err) {
//Set error response data axios type
if (err?.response?.data) {
_instance.error = err.response.data;
} else {
//Add internal error
_instance.error = { internal: 'Something fail, try again' };
}
}
//Close loading
if (!_instance.error) _instance.loading = false;
//Add first mount
if (!_instance.firstMount) _instance.firstMount = true;
//Complete query instance
setInstance(_instance);
};
useEffect(() => {
/**
* Fetch data on mount
*/
if (fetchOnMount) onFetch();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return [onFetch, useMemo(() => instance, [instance])];
};
To me it seems that an unwanted line break was introduced in the description on useHooks.com in the typescript variant leading to the file not compiling:
"idle" | "pending" | "success" | "error"
("idle");
should be in one line, I think.
Great stuff nevertheless!!!
@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.
@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?
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!
Is it ideal to use a function as a useCallback
dependency? Function is created fresh in every render (unless handled).
The init value of state should be depend on immediate variable:
const [status, setStatus] = useState(() => immediate ? 'pending' : 'idle' );
@Yey007, how can a response be a function?