我正在使用useRef
来保存 prop 的最新值,以便稍后可以在异步调用的回调(例如 onClick 处理程序)中访问它。我使用的是 ref 而不是将value
放在 useCallback 依赖项列表中,因为我希望该值会经常更改(当此组件使用新value
重新呈现时),但很少调用 onClick 处理程序,因此每次值更改时都不值得为元素分配新的事件侦听器。
function MyComponent({ value }) {
const valueRef = useRef(value);
valueRef.current = value; // is this ok?
const onClick = useCallback(() => {
console.log("the latest value is", valueRef.current);
}, []);
...
}
React 严格模式的文档让我相信在render()
中执行副作用通常是不安全的。
由于上述方法 [包括类组件
render()
和函数组件主体] 可能会被多次调用,因此它们不包含副作用非常重要。忽略此规则可能会导致各种问题,包括内存泄漏和无效的应用程序状态。
事实上,当我使用ref 访问旧值时,我在严格模式下遇到了问题。
我的问题是:从渲染函数分配valueRef.current = value
的"副作用"是否有任何担忧?例如,是否存在回调会收到过时值(或来自尚未提交的"未来"渲染的值)的情况?
我能想到的一种选择是确保在组件渲染后更新 ref 的useEffect
,但从表面上看,这看起来没有必要。
function MyComponent({ value }) {
const valueRef = useRef(value);
useEffect(() => {
valueRef.current = value; // is this any safer/different?
}, [value]);
const onClick = useCallback(() => {
console.log("the latest value is", valueRef.current);
}, []);
...
}
例如,是否存在回调会收到过时值(或来自尚未提交的"未来"渲染的值)的情况?
括号是主要关注点。
目前,render
(和功能组件)调用与实际 DOM 更新之间存在一对一的对应关系。 (即提交)
但是很长一段时间以来,React 团队一直在谈论一种"并发模式",在该模式下,更新可能会开始(render
被调用),但随后会被更高优先级的更新打断。
在这种情况下,如果 ref 在被取消的渲染中更新,则 ref 最终可能会与渲染组件的实际状态不同步。
这已经假设了很长时间,但刚刚宣布一些并发模式更改将以选择加入的方式登陆 React 18,使用startTransition
API。 (也许还有其他一些)
实际上,这在多大程度上是一个实际问题? 很难说。startTransition
是选择加入的,所以如果你不使用它,你可能很安全。 无论如何,许多参考更新都将相当"安全"。
但如果可以的话,
最好谨慎行事。UPDATE:现在,react.dev 文档还说你不应该这样做:
在渲染过程中不要写入或读取
ref.current
,初始化除外。这使得组件的行为不可预测。
通过上面的初始化,它们意味着这样的模式:
function Video() {
const playerRef = useRef(null);
if (playerRef.current === null) {
playerRef.current = new VideoPlayer();
}
....
据我所知,这是安全的,但你只需要知道,当 React "感觉像"渲染你的组件时,可能会对 ref-boxed 值进行更改,而不一定是确定性的。
这看起来很像react-use
的useLatest
钩子(docs),在这里复制,因为它是微不足道的:
import { useRef } from 'react';
const useLatest = <T>(value: T): { readonly current: T } => {
const ref = useRef(value);
ref.current = value;
return ref;
};
export default useLatest;
如果它适用于react-use
,我认为这对你也很好。
function MyComponent({ value }) { const valueRef = useRef(value); valueRef.current = value; // is this ok? const onClick = useCallback(() => { console.log("the latest value is", valueRef.current); }, []); ... }
我在这里并没有真正看到问题,因为每个渲染周期都会发生valueRef.current = value
问题。它并不昂贵,但它会在每个渲染周期发生。
如果您使用useEffect
钩子,那么您至少将 ref 值设置为仅在prop实际更改时才设置的次数。
function MyComponent({ value }) { const valueRef = useRef(value); useEffect(() => { valueRef.current = value; }, [value]); const onClick = useCallback(() => { console.log("the latest value is", valueRef.current); }, []); ... }
由于useEffect
在组件生命周期中的工作方式,我建议坚持使用useEffect
钩子并保持正常的 React 模式。使用useEffect
钩子还可以为每个实际渲染周期提供更确定的值,即"提交阶段"与可以取消、中止、召回等的"渲染阶段"......
不过好奇的是,如果你只想要最新的value
道具值,只需直接引用value
道具,它将永远是当前的最新值。将其添加到useCallback
挂钩的依赖项中。这本质上是您使用更新 refuseEffect
完成的工作,但以更清晰的方式。
function MyComponent({ value }) {
...
const onClick = useCallback(() => {
console.log("the latest value is", value);
}, [value]);
...
}
如果你真的总是想要最新的突变值,那么是的,跳过useCallback
依赖项,跳过useEffect
,并改变你想要/需要的 ref,只需引用当前 ref 值在调用回调时