I'm having trouble deciding how to trigger an API call imperatively, for example, on a button click.
I'm unsure what is the proper approach with hooks, because there seems to be more than one method, but I don't understand which is the "best" approach and the eventual implications.
I've found the following examples that are simple enough and do what I want:
function SomeFunctionComponent() {
const [fakeData, setFakeData] = useState(0);
const [trigger, setTrigger] = useState(false);
async function fetchData() {
if (!trigger) return;
const newData = await someAPI.fetch();
setTrigger(false);
setFakeData(newData);
}
useEffect(() => {
fetchData();
}, [trigger]);
return (
<React.Fragment>
<p>{fakeData}</p>
<button onClick={() => setTrigger(!trigger)}>Refresh</button>
</React.Fragment>
);
}
Example
function SomeFunctionComponent() {
const [fakeData, setFakeData] = useState(0);
async function fetchData() {
const newData = await someAPI.fetch();
setFakeData(newData);
}
return (
<React.Fragment>
<p>{fakeData}</p>
<button onClick={fetchData}>Refresh</button>
</React.Fragment>
);
}
Example
There are also other approaches that leverage useCallback() but as far as I understood they are useful to avoid re-rendering child components when passing callbacks down and are equivalent to the second example.
I think that the useEffect approach is useful only when something has to run on component mount and programmatically, but having what essentially is a dummy value to trigger a side-effect looks verbose.
Just calling the function looks pragmatic and simple enough but I'm not sure if a function component is allowed to perform side-effects during render.
Which approach is the most idiomatic and correct to have imperative calls using hooks in React ?
React can run this async function but can not run the cleanup function. Don't use raw async function directly in the useEffect. useEffect(async () => { console.
Setting State Is Asynchronous React sets this state asynchronously, which means that the state is not changed immediately but after a few milliseconds. React sets its state asynchronously because doing otherwise can result in an expensive operation. Making it synchronous might leave the browser unresponsive.
Example: Pass a Button Value to an Inline Function Notice the value e that's returned from the onClick event handler: import React from 'react'; function App() { return ( <button value="hello!" onClick={e => alert(e. target. value)}> Click me!
Either way, we're now safe to use async functions inside useEffect hooks. Now if/when you want to return a cleanup function, it will get called and we also keep useEffect nice and clean and free from race conditions. Enjoy using async functions with React's useEffect from here on out!
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>
}
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>
);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With