使用 React.useMemo 进行异步调用

Posted

技术标签:

【中文标题】使用 React.useMemo 进行异步调用【英文标题】:Asynchronous calls with React.useMemo 【发布时间】:2020-08-28 07:03:41 【问题描述】:

场景相对简单:我们在远程服务器上进行了长时间运行的按需计算。我们想记住结果。即使我们从远程资源异步获取,这也不是副作用,因为我们只想将此计算的结果显示给用户,而且我们绝对不希望在每次渲染时都这样做。

问题:React.useMemo 好像不直接支持 Typescript 的 async/await 会返回一个promise:

//returns a promise: 
let myMemoizedResult = React.useMemo(() => myLongAsyncFunction(args), [args])
//also returns a promise:
let myMemoizedResult = React.useMemo(() => (async () => await myLongAsyncFunction(args)), [args])

等待异步函数的结果并使用 React.useMemo 记忆结果的正确方法是什么?我在普通的 JS 中使用了常规的 Promise,但在这些类型的情况下仍然很难使用它们。

我尝试过其他方法,例如 memoize-one,但问题似乎是 this 上下文由于 React 函数组件的工作方式而发生变化 break the memoization,这就是我尝试使用的原因React.useMemo.

也许我正在尝试在此处的圆孔中安装一个方形钉 - 如果是这样的话,我也很高兴知道这一点。现在我可能只是要推出我自己的记忆功能。

编辑:我认为部分原因是我在使用 memoize-one 时犯了一个不同的愚蠢错误,但我仍然有兴趣在 React.memo 上知道答案。

这是一个 sn-p - 想法不是直接在渲染方法中使用记忆结果,而是作为以事件驱动的方式引用的东西,即在计算按钮单击时。

export const MyComponent: React.FC = () => 
    let [arg, setArg] = React.useState('100');
    let [result, setResult] = React.useState('Not yet calculated');

    //My hang up at the moment is that myExpensiveResultObject is 
    //Promise<T> rather than T
    let myExpensiveResultObject = React.useMemo(
        async () => await SomeLongRunningApi(arg),
        [arg]
    );

    const getResult = () => 
        setResult(myExpensiveResultObject.interestingProperty);
    

    return (
        <div>
            <p>Get your result:</p>
            <input value=arg onChange=e => setArg(e.target.value)></input>
            <button onClick=getResult>Calculate</button>
            <p>`Result is $result`</p>
        </div>);

【问题讨论】:

为什么不let myMemoizedResult = await React.useMemo(() =&gt; myLongAsyncFunction(args), [args]) @GazihanAlankus 这难道不会导致一个记忆化的承诺每次都回调到长时间运行的异步函数,即使 args 没有改变? 抱歉,我不了解 useMemo,我只是认为您通常遇到 async/await 问题。似乎他们反对您尝试做的事情:digitalocean.com/community/tutorials/…“您不会希望 useMemo 触发任何副作用或任何异步调用。将这两者包含在 useEffect 中会更有意义。”跨度> 感谢您的回复 - 这就是为什么我包含关于这不是副作用的部分。 useEffect 在每次渲染时检查参数,这不是这里想要的行为,因为用户可能希望在调用长时间运行的计算之前更改多个输入.. 我认为我需要在原始问题中添加更多细节,以便更清楚地说明我想要做什么。 【参考方案1】:

您真正想要的是在异步调用结束后重新渲染您的组件。仅靠记忆不会帮助您实现这一目标。相反,您应该使用 React 的状态 - 它会保留异步调用返回的值,并允许您触发重新渲染。

此外,触发异步调用是一种副作用,因此不应在渲染阶段执行 - 既不在组件函数的主体内部,也不在 useMemo(...) 内部,这也发生在渲染阶段。相反,所有副作用都应该在 useEffect 内触发。

这是完整的解决方案:

const [result, setResult] = useState()

useEffect(() => 
  let active = true
  load()
  return () =>  active = false 

  async function load() 
    setResult(undefined) // this is optional
    const res = await someLongRunningApi(arg1, arg2)
    if (!active)  return 
    setResult(res)
  
, [arg1, arg2])

这里我们调用useEffect内部的异步函数。请注意,您不能在 useEffect 内进行整个回调异步 - 这就是为什么我们在内部声明一个异步函数 load 并在不等待的情况下调用它。

一旦args 更改之一,效果将重新运行 - 在大多数情况下,这是您想要的。因此,如果您在渲染时重新计算它们,请务必记住 args。执行setResult(undefined) 是可选的 - 您可能希望将上一个结果保留在屏幕上,直到获得下一个结果。或者你可以做类似setLoading(true) 这样用户知道发生了什么。

使用active 标志很重要。没有它,您将自己暴露于等待发生的竞争条件:第二个异步函数调用可能在第一个完成之前完成:

    开始第一次通话 开始第二次通话 第二次通话结束,setResult() 发生 第一次调用结束,setResult() 再次发生,覆盖 过时的正确结果

并且您的组件最终处于不一致的状态。我们通过使用useEffect 的清理功能来重置active 标志来避免这种情况:

    设置active#1 = true,开始第一次通话 arg 改变,清理函数被调用,设置active#1 = false 设置active#2 = true,开始第二次通话 第二次通话结束,setResult() 发生 第一次通话结束,setResult() 不会发生,因为 active#1false

【讨论】:

重新格式化这个答案怎么样?也许# Do this (good impl., comments if needed) # Watch out (bad example, with comments) 感谢您的邀请,虽然点击编辑,但它显示“建议的编辑队列已满”,所以要么我被禁止或限制编辑,要么是这个答案有一个完整的队列。无论如何,我看到你做了一些改进:)很好! @SimonB。谢谢。我终于完全重写了答案,希望现在更好 这应该是异步的正确答案。它完美地工作@SimonB。【参考方案2】:

我认为 React 特别提到 useMemo 不应该用于管理像异步 API 调用这样的副作用。它们应该在 useEffect 挂钩中进行管理,其中设置了适当的依赖项以确定是否应该重新运行它们。

【讨论】:

【参考方案3】:

编辑:由于调用的异步性质,我下面的原始答案似乎有一些意想不到的副作用。相反,我会尝试考虑记住服务器上的实际计算,或者使用自写的闭包来检查 arg 是否没有改变。否则,您仍然可以使用 useEffect 之类的东西,如下所述。

我认为问题在于async 函数总是隐含地返回一个承诺。既然是这样,你可以直接await结果解开promise:

const getResult = async () => 
  const result = await myExpensiveResultObject;
  setResult(result.interestingProperty);
;

查看example codesandbox here。

我确实认为,虽然更好的模式可能是利用 useEffect,它依赖于某些状态对象,在这种情况下,该对象仅在按钮单击时设置,但似乎 useMemo 应该作为嗯。

【讨论】:

以上是关于使用 React.useMemo 进行异步调用的主要内容,如果未能解决你的问题,请参考以下文章

react——useMemo——useCallback——性能优化——React.memo

React Hooks-useMemo篇

如何使用 Spring5 WebClient 进行异步调用

将非异步方法(进行网络调用)包装到异步中

在 PCL 中使用 HttpClient 进行异步调用

使用 Vue.js 进行异步/等待 axios 调用