Imperatively trigger an asynchronous request with React hooks
The accepted answer does actually break the rules of hooks. As the click is Asynchronous, which means other renders might occur during the fetch call which would create SideEffects and possibly the dreaded Invalid Hook Call Warning.
We can fix it by checking if the component is mounted before calling setState()
functions. Below is my solution, which is fairly easy to use.
Hook function
function useApi(actionAsync, initialResult) {
const [loading, setLoading] = React.useState(false);
const [result, setResult] = React.useState(initialResult);
const [fetchFlag, setFetchFlag] = React.useState(0);
React.useEffect(() => {
if (fetchFlag == 0) {
// Run only after triggerFetch is called
return;
}
let mounted = true;
setLoading(true);
actionAsync().then(res => {
if (mounted) {
// Only modify state if component is still mounted
setLoading(false);
setResult(res);
}
})
// Signal that compnoent has been 'cleaned up'
return () => mounted = false;
}, [fetchFlag])
function triggerFetch() {
// Set fetchFlag to indirectly trigger the useEffect above
setFetchFlag(Math.random());
}
return [result, triggerFetch, loading];
}
Usage in React Hooks
function MyComponent() {
async function fetchUsers() {
const data = await fetch("myapi").then((r) => r.json());
return data;
}
const [fetchResult, fetchTrigger, fetchLoading] = useApi(fetchUsers, null);
return (
<div>
<button onClick={fetchTrigger}>Refresh Users</button>
<p>{fetchLoading ? "Is Loading" : "Done"}</p>
<pre>{JSON.stringify(fetchResult)}</pre>
</div>
);
}
The first thing I do when I try to figure out the best way to write something is to look at how I would like to use it. In your case this code:
<React.Fragment>
<p>{fakeData}</p>
<button onClick={fetchData}>Refresh</button>
</React.Fragment>
seems the most straightforward and simple. Something like <button onClick={() => setTrigger(!trigger)}>Refresh</button>
hides your intention with details of the implementation.
As to your question remark that "I'm not sure if a function component is allowed to perform side-effects during render." , the function component isn't doing side-effects during render, since when you click on the button a render does not occur. Only when you call setFakeData
does a render actually happen. There is no practical difference between implementation 1 and implementation 2 in this regard since in both only when you call setFakeData
does a render occur.
When you start generalizing this further you'll probably want to change this implementation all together to something even more generic, something like:
function useApi(action,initial){
const [data,setData] = useState({
value:initial,
loading:false
});
async function doLoad(...args){
setData({
value:data.value,
loading:true
});
const res = await action(...args);
setData({
value:res,
loading:false
})
}
return [data.value,doLoad,data.loading]
}
function SomeFunctionComponent() {
const [data,doLoad,loading] = useApi(someAPI.fetch,0)
return <React.Fragment>
<p>{data}</p>
<button onClick={doLoad}>Refresh</button>
</React.Fragment>
}