使用 React hooks 命令式触发异步请求

Imperatively trigger an asynchronous request with React hooks

我无法决定如何触发 API 调用 强制性 ,例如,在单击按钮时。
我不确定使用钩子的正确方法是什么,因为似乎有不止一种方法,但我不明白哪种是 "best" 方法和最终影响。

我发现以下示例足够简单并且可以做我想做的事:

使用带触发值的 useEffect()

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

只需调用 API 然后 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

还有其他利用 useCallback() 的方法,但据我了解,它们有助于在向下传递回调时避免重新呈现子组件,并且等同于第二个示例。

我认为 useEffect 方法只有在某些东西必须以编程方式 运行 挂载组件 时才有用,但本质上是一个虚拟值来触发副作用看起来很冗长。
只是调用函数看起来很实用和简单,但我不确定是否允许函数组件在渲染期间执行副作用。

对于在 React 中使用钩子进行命令式调用,哪种方法最惯用且最正确?

当我试图找出最好的写作方式时,我做的第一件事就是看看我想如何使用它。在您的情况下,此代码:

    <React.Fragment>
      <p>{fakeData}</p>
      <button onClick={fetchData}>Refresh</button>
    </React.Fragment>

看起来是最直接最简单的。 <button onClick={() => setTrigger(!trigger)}>Refresh</button> 之类的内容通过实施细节隐藏了您的意图。

关于您的问题,请注意 "I'm not sure if a function component is allowed to perform side-effects during render." ,功能组件在渲染期间不会产生副作用,因为当您单击按钮时不会发生渲染。只有当您调用 setFakeData 时,渲染才会真正发生。在这方面,实现 1 和实现 2 之间没有实际区别,因为在这两种情况下,只有当您调用 setFakeData 时才会发生渲染。

当您开始进一步推广时,您可能希望将此实现一起更改为更通用的东西,例如:

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

接受的答案实际上打破了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

我们可以通过在调用 setState() 函数之前检查组件是否 mounted 来修复它。以下是我的解决方案,相当容易使用。

钩子函数

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

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