博文 《"How React Uses Closures to Avoid Bugs"》(epicreact.dev/how-react-u…) 解释了当 React 从类和生命周期转换到函数和 hooks 时所做的一些权衡;我想在这个主题上深入一下。
在那篇文章中,有以下示例:
function useDebounce(callback, delay) { const callbackRef = React.useRef(callback) React.useLayoutEffect(() => { callbackRef.current = callback }) return React.useMemo( () => debounce((args) => callbackRef.current(args), delay), [delay], ) }
hook 的创始人 Yago 喜欢称之为“最新Ref模式”的模式。
这个模式本身非常简单。这就是模式的部分:
const callbackRef = React.useRef(callback) React.useLayoutEffect(() => { callbackRef.current = callback })
就是这样而已。
那么为什么要这样做呢?好吧,让我们考虑何时使用 useRef
。当你想跟踪一个值但不想在更新它时触发重新渲染时,就可以使用useRef
。所以在例子中,我们正试图跟踪callback
。这样做的原因是,我们希望始终调用最新版本的callback
,而不是旧渲染中的版本。
但是为什么不使用useState
呢?是否可以在实际的状态值中跟踪这个最新的回调值?我们不想使用useState
,因为当更新到最新值时,不需要触发组件重新渲染。实际上,在我们的例子中,如果尝试这样做,将触发一个无限循环(试试看吧😈)。
由于不需要也不希望在将callback
更新为最新值时重新渲染组件,这意味着我们也不需要(而且实际上不应该)将它包含在useEffect
、useCallback
或例子的useMemo
依赖数组中。这是一个重要的观点,因此我想深入探讨一下。
遵循eslint-plugin-react-hooks/exhaustive-deps
规则并始终包括所有依赖项非常重要。但是您应该跳过引用的“current”值。所以永远不要这样做:
// ❌ 永远不要这样做 React.useEffect(() => {}, [ref.current])
这是因为更新引用不会触发重新渲染,所以 React 无法在更新引用时调用 effect 回调函数或更新记忆化值。因此,如果将 ref.current
包含在依赖项数组中,你将触发怪异且难以调试的行为。顺便说一下,由于 ref
本身是一个稳定的对象,因此是否在依赖项数组中包含 ref
对象本身并不重要:
// 🤷♂️ 是否包含 ref 都没关系 React.useEffect(() => {}, [ref])
但是,如果没有包含所有非 ref 依赖项,可能会遇到一些严重的错误,因此请不要忽略 www.npmjs.com/package/esl… 规则。
结论
在到处使用“最新 Ref 模式”之前,我建议您充分了解您正在规避的内容,因此,如果还没有这样做,请仔细阅读 《React 如何使用闭包避免错误》(epicreact.dev/how-react-u…