使用 React.useMemo 进行异步调用



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

问题:React.useMemo似乎不直接支持Typescript的async/await,并且会返回一个承诺:

//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中使用了常规承诺,但在这些类型的情况下仍然难以应对。

我已经尝试了其他方法,例如 memoize-one,但问题似乎是由于 React 函数组件的工作方式破坏了记忆,this上下文发生了变化,这就是我尝试使用 React.useMemo 的原因。

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

编辑:我认为部分原因是我在记住一个时犯了一个不同的愚蠢错误,但我仍然有兴趣知道这里的答案 wrt 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>);
}

您真正想要的是异步调用结束后重新渲染组件。仅靠记忆并不能帮助你实现这一目标。相反,你应该使用 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声明一个异步函数并在不等待的情况下调用它。

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

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

  1. 开始第一个呼叫
  2. 开始第二个呼叫
  3. 第二次调用完成,setResult()发生
  4. 第一次调用完成,setResult()再次发生,覆盖 过时的正确结果

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

  1. 设置active#1 = true,开始第一个呼叫
  2. 参数更改,调用清理函数,设置active#1 = false
  3. 设置active#2 = true,开始第二次调用
  4. 第二次调用完成,setResult()发生
  5. 第一次调用完成,setResult()不会发生,因为active#1false

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

我认为问题在于async函数总是隐式返回一个承诺。既然是这种情况,可以直接await结果来解开承诺:

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

请参阅此处的示例代码沙盒。

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

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

相关内容

  • 没有找到相关文章

最新更新