使用 React.useMemo 的异步调用
Asynchronous calls with React.useMemo
场景相对简单:我们有一个长-运行,在远程服务器上发生的按需计算。我们想记住结果。即使我们从远程资源异步获取,这也不是副作用,因为我们只想将计算结果显示给用户,我们绝对不想在每次渲染时都这样做。
问题:好像React.useMemo不直接支持Typescript的async/await,会return一个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。
这里有一个片段 - 这个想法不是直接在渲染方法中使用记忆的结果,而是作为以事件驱动的方式引用的东西,即点击计算按钮。
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>);
}
编辑: 由于调用的异步性质,我下面的原始答案似乎有一些意想不到的副作用。相反,我会尝试考虑在服务器上记住实际计算,或者使用自己编写的闭包来检查 arg
是否没有改变。否则你仍然可以使用 useEffect
之类的东西,如下所述。
我认为问题在于 async
函数总是隐含地 return 一个承诺。既然是这样,可以直接await
结果来解包promise:
const getResult = async () => {
const result = await myExpensiveResultObject;
setResult(result.interestingProperty);
};
我确实认为,虽然更好的模式可能是利用 useEffect
,它依赖于某些状态对象,在这种情况下,该对象仅在单击按钮时设置,但看起来 useMemo
也应该有效。
您真正想要的是在异步调用结束后重新呈现您的组件。仅靠记忆并不能帮助您实现这一目标。相反,您应该使用 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
里面的async函数。请注意,您不能在 useEffect
内部使整个回调异步 - 这就是为什么我们在内部声明一个异步函数 load
并在不等待的情况下调用它。
一旦 arg
之一发生变化,效果将重新 运行 - 这在大多数情况下是您想要的。因此,如果您在渲染时重新计算它们,请确保记住 arg
s。执行 setResult(undefined)
是可选的 - 您可能希望在获得下一个结果之前将上一个结果保留在屏幕上。或者你可以做类似 setLoading(true)
的事情,这样用户就知道发生了什么。
使用 active
标志很重要。没有它,您将面临等待发生的竞争条件:第二个异步函数调用可能在第一个异步函数调用完成之前完成:
- 开始第一次通话
- 开始第二次通话
- 第二次通话结束,
setResult()
发生
- 第一次调用完成,
setResult()
再次发生,覆盖
一个陈旧的正确结果
并且您的组件最终处于不一致状态。我们通过使用 useEffect
的清理函数来重置 active
标志来避免这种情况:
- 设置
active#1 = true
,开始第一次调用
- arg 改变,调用清理函数,设置
active#1 = false
- 设置
active#2 = true
,开始第二次调用
- 第二次通话结束,
setResult()
发生
- 第一次调用完成,
setResult()
不会发生,因为 active#1
是 false
我认为 React 特别提到 useMemo 不应该用于管理像异步 API 调用这样的副作用。它们应该在 useEffect
挂钩中进行管理,其中设置了适当的依赖项以确定它们是否应该重新 运行。
场景相对简单:我们有一个长-运行,在远程服务器上发生的按需计算。我们想记住结果。即使我们从远程资源异步获取,这也不是副作用,因为我们只想将计算结果显示给用户,我们绝对不想在每次渲染时都这样做。
问题:好像React.useMemo不直接支持Typescript的async/await,会return一个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。
这里有一个片段 - 这个想法不是直接在渲染方法中使用记忆的结果,而是作为以事件驱动的方式引用的东西,即点击计算按钮。
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>);
}
编辑: 由于调用的异步性质,我下面的原始答案似乎有一些意想不到的副作用。相反,我会尝试考虑在服务器上记住实际计算,或者使用自己编写的闭包来检查 arg
是否没有改变。否则你仍然可以使用 useEffect
之类的东西,如下所述。
我认为问题在于 async
函数总是隐含地 return 一个承诺。既然是这样,可以直接await
结果来解包promise:
const getResult = async () => {
const result = await myExpensiveResultObject;
setResult(result.interestingProperty);
};
我确实认为,虽然更好的模式可能是利用 useEffect
,它依赖于某些状态对象,在这种情况下,该对象仅在单击按钮时设置,但看起来 useMemo
也应该有效。
您真正想要的是在异步调用结束后重新呈现您的组件。仅靠记忆并不能帮助您实现这一目标。相反,您应该使用 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
里面的async函数。请注意,您不能在 useEffect
内部使整个回调异步 - 这就是为什么我们在内部声明一个异步函数 load
并在不等待的情况下调用它。
一旦 arg
之一发生变化,效果将重新 运行 - 这在大多数情况下是您想要的。因此,如果您在渲染时重新计算它们,请确保记住 arg
s。执行 setResult(undefined)
是可选的 - 您可能希望在获得下一个结果之前将上一个结果保留在屏幕上。或者你可以做类似 setLoading(true)
的事情,这样用户就知道发生了什么。
使用 active
标志很重要。没有它,您将面临等待发生的竞争条件:第二个异步函数调用可能在第一个异步函数调用完成之前完成:
- 开始第一次通话
- 开始第二次通话
- 第二次通话结束,
setResult()
发生 - 第一次调用完成,
setResult()
再次发生,覆盖 一个陈旧的正确结果
并且您的组件最终处于不一致状态。我们通过使用 useEffect
的清理函数来重置 active
标志来避免这种情况:
- 设置
active#1 = true
,开始第一次调用 - arg 改变,调用清理函数,设置
active#1 = false
- 设置
active#2 = true
,开始第二次调用 - 第二次通话结束,
setResult()
发生 - 第一次调用完成,
setResult()
不会发生,因为active#1
是false
我认为 React 特别提到 useMemo 不应该用于管理像异步 API 调用这样的副作用。它们应该在 useEffect
挂钩中进行管理,其中设置了适当的依赖项以确定它们是否应该重新 运行。