灏天阁

React 中的 '最新 Ref 模式'

· Yin灏

原文: https://epicreact.dev/the-latest-ref-pattern-in-react/

博文 《“How React Uses Closures to Avoid Bugs”》(https://epicreact.dev/how-react-uses-closures-to-avoid-bugs) 解释了当 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更新为最新值时重新渲染组件,这意味着我们也不需要(而且实际上不应该)将它包含在useEffectuseCallback或例子的useMemo依赖数组中。这是一个重要的观点,因此我想深入探讨一下。

遵循eslint-plugin-react-hooks/exhaustive-deps规则并始终包括所有依赖项非常重要。但是您应该跳过引用的“current”值。所以永远不要这样做:

// ❌ 永远不要这样做
React.useEffect(() => {}, [ref.current]);

这是因为更新引用不会触发重新渲染,所以 React 无法在更新引用时调用 effect 回调函数或更新记忆化值。因此,如果将 ref.current 包含在依赖项数组中,你将触发怪异且难以调试的行为。顺便说一下,由于 ref 本身是一个稳定的对象,因此是否在依赖项数组中包含 ref 对象本身并不重要:

// 🤷‍♂️ 是否包含 ref 都没关系
React.useEffect(() => {}, [ref]);

但是,如果没有包含所有非 ref 依赖项,可能会遇到一些严重的错误,因此请不要忽略 https://www.npmjs.com/package/eslint-plugin-react-hooks 规则。

结论

在到处使用“最新 Ref 模式”之前,我建议您充分了解您正在规避的内容,因此,如果还没有这样做,请仔细阅读 《React 如何使用闭包避免错误》(https://epicreact.dev/how-react-uses-closures-to-avoid-bugs)。这将帮助您更好地了解何时可以使用此特定模式。

- Book Lists -