Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Imperatively trigger an asynchronous request with React hooks

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:

Using useEffect() with a trigger value

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

Just calling the API and then setState()

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 ?

like image 482
Gwinn Avatar asked Apr 17 '19 11:04

Gwinn


People also ask

Can a React hook be an async function?

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.

Are React Hooks synchronous or asynchronous?

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.

How do you write onClick in React Hooks?

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!

Can we use async await with the useEffect hook?

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!


2 Answers

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>
  }
like image 169
Alon Bar David Avatar answered Oct 24 '22 09:10

Alon Bar David


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>
  );
}
like image 7
Ben Winding Avatar answered Oct 24 '22 11:10

Ben Winding