使用 React.useMemo 进行异步调用

IT技术 reactjs async-await react-usememo
2021-05-17 13:01:23

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

问题: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 函数组件的工作方式导致上下文发生了变化,从而破坏了 memoization,这就是我尝试使用 React.useMemo 的原因。

也许我想在这里的一个圆孔中安装一个方钉——如果是这样的话,也知道这一点会很好。现在我可能只是要推出我自己的记忆功能。

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

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

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>);
}
3个回答

您真正想要的是在异步调用结束后重新渲染您的组件。仅靠记忆并不能帮助您实现这一目标。相反,您应该使用 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请注意,您不能在useEffectasync内部进行整个回调- 这就是为什么我们load在内部声明一个 async 函数并在不等待的情况下调用它。

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

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

  1. 开始第一次通话
  2. 开始第二次通话
  3. 第二次通话结束,setResult()发生
  4. 第一次调用完成,setResult()再次发生,用陈旧的结果覆盖正确的结果

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

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

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

我相信问题在于async函数总是隐式地返回一个Promise。既然如此,就可以直接await将结果解开promise:

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

在此处查看示例代码和框

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

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