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>
  }