【译&补】使用ref回调替代useRef吧

在React中,useRef和useEffect常用于管理组件的可变值和副作用,但在响应DOM元素挂载时,使用useCallback作为ref的回调函数更为有效。useCallback确保在元素挂载和卸载时被调用,避免了因useRef导致的生命周期回调未触发的问题。文中还介绍了其他几种优化Node操作的方法,如使用useState、useStateRef和useRefWithCallback,以提高性能和管理复杂性。

原文:useCallback Might Be What You Meant By useRef & useEffect

在React中,useRefuseEffect是常用的钩子函数,用于不同的目的。

useRef通常用于保存在组件渲染之间持久存在的可变值,而useEffect用于执行诸如数据获取、订阅或DOM操作等副作用。

然而,当涉及对React元素挂载做出响应时,有一个更好的选择:useCallback

尝试在Effect中使用Ref

如果你想要对React元素在DOM中的挂载做出响应,你可能会尝试使用useRef来获取它的引用,并使用useEffect来响应其挂载和卸载。但这样做是无效的。

这是因为当组件被(卸载)挂载并通过useRef连接到ref.current时,ref.current的更改并没有触发回调或重新渲染,自然也不会触发预期的生命周期回调。

甚至react-hooks 的ESLint规则也会对此发出警告。请注意,无论是ref还是ref.current作为useEffect的依赖项,都不会触发它的执行(这是useRef的特性决定的)。

下面的例子展示了上面的方法确实不可行的(在Code Sandbox中尝试)。

export default function App() {
  const [count, setCount] = useState(1);
  const shouldShowImageOfCat = count % 3 === 0;

  const [catInfo, setCatInfo] = useState(false);

  // notice how none of the deps of useEffect
  // manages to trigger the hook in time
  const catImageRef = useRef();
  useEffect(() => {
    console.log(catImageRef.current);
    setCatInfo(catImageRef.current?.getBoundingClientRect());
    // notice the warning below
  }, [catImageRef, catImageRef.current]);

  return (
    <div className="App">
      <h1>useEffect & useRef vs useCallback</h1>
      <p>
        An image of a cat would appear on every 3rd render.
        <br />
        <br />
        Would our hook be able to make the emoji see it?
        <br />
        <br />
        {catInfo ? "😂" : "😩"} - I {catInfo ? "" : "don't"} see the cat 🐈
        {catInfo ? `, it's height is ${catInfo.height}` : ""}!
      </p>
      <input disabled value={`render #${count}`} />
      <button onClick={() => setCount((c) => c + 1)}>next render</button>
      <br />
      {shouldShowImageOfCat ? (
        <img
          ref={catImageRef}
          src={catImageUrl}
          alt="cat"
          width="50%"
          style={{ padding: 10 }}
        />
      ) : (
        ""
      )}
    </div>
  );
}

那我们应该怎么做呢?使用useCallback。(参考react官方文档

使用useCallback替代Ref

我们可以依赖于将一个普通函数通过useCallback包装后传递给ref,并对它返回的最新DOM节点引用做出反应。

下面的代码是一个例子(在Code Sandbox中尝试):

export default function App() {
  const [count, setCount] = useState(1);
  const shouldShowImageOfCat = count % 3 === 0;

  const [catInfo, setCatInfo] = useState(false);

  // notice how this is a useCallback
  // that's used as the "ref" of the image below
  const catImageRef = useCallback((catImageNode) => {
    console.log(catImageNode);
    setCatInfo(catImageNode?.getBoundingClientRect());
  }, []);

  return (
    <div className="App">
      <h1>useEffect & useRef vs useCallback</h1>
      <p>
        An image of a cat would appear on every 3rd render.
        <br />
        <br />
        Would our hook be able to make the emoji see it?
        <br />
        <br />
        {catInfo ? "😂" : "😩"} - I {catInfo ? "" : "don't"} see the cat 🐈
        {catInfo ? `, it's height is ${catInfo.height}` : ""}!
      </p>
      <input disabled value={`render #${count}`} />
      <button onClick={() => setCount((c) => c + 1)}>next render</button>
      <br />
      {shouldShowImageOfCat ? (
        <img
          ref={catImageRef}
          src={catImageUrl}
          alt="cat"
          width="50%"
          style={{ padding: 10 }}
        />
      ) : (
        ""
      )}
    </div>
  );
}

在上面的例子中,我们可以看到useCallback被传入了ref参数中,这个过程中我们需要注意以下两点:

  • ref函数保证在元素挂载和卸载时被调用,即使是第一次挂载,甚至在父元素卸载导致的情况下也是如此。
  • 务必使用*useCallback*来包装ref回调函数。因为如果没有使用useCallback,在ref回调函数中引发重新渲染,会导致ref回调函数再次以null触发,可能导致无限循环,这是由于React内部机制所致。

其他方案

实际上这种模式可以以多种方式使用,以下三种方法渐进地对Node操作进行了优化:

useState

由于useState是在渲染之间保持一致的函数,它也可以用作ref。在这种情况下,整个节点将保存在state中。

作为一个状态,当它发生变化时,它会触发重新渲染,并且可以安全地在渲染结果和useEffect的依赖项中使用该状态:

const [node, setRef] = useState(null);

useEffect(() => {
  if (!node) {
    console.log('unmounted!');
    return null;
  }

  console.log('mounted');

  const fn = e => console.log(e);

  node.addEventListener('mousedown', fn);
  return () => node.removeEventListener('mousedown', fn);
}, [node])

useStateRef

访问DOM是一项昂贵的操作,因此我们希望尽可能少地进行这样的操作。

如果你不需要像之前的钩子函数中那样保存整个节点,最好只在一个状态中保存其中的一部分:

// the hook
function useStateRef(processNode) {
  const [node, setNode] = useState(null);
  const setRef = useCallback(newNode => {
    setNode(processNode(newNode));
  }, [processNode]);
  return [node, setRef];
}

// how it's used
const [clientHeight, setRef] = useStateRef(node => (node?.clientHeight || 0));

useEffect(() => {
  console.log(`the new clientHeight is: ${clientHeight}`);
}, [clientHeight])

// <div ref={setRef}....

// <div>the current height is: {clientHeight}</div>

useRefWithCallback

然而,有时为了性能考虑,你可以在使用ref的元素挂载和卸载时避免触发重新渲染。

下面的钩子函数不会将节点保存在状态中。它直接响应挂载和卸载,因此不会触发任何重新渲染。

// the hook
function useRefWithCallback(onMount, onUnmount) {
  const nodeRef = useRef(null);

  const setRef = useCallback(node => {
    if (nodeRef.current) {
      onUnmount(nodeRef.current);
    }

    nodeRef.current = node;

    if (nodeRef.current) {
      onMount(nodeRef.current);
    }
  }, [onMount, onUnmount]);

  return setRef;
}

const onMouseDown = useCallback(e => console.log('hi!', e.target.clientHeight), []);

const setDivRef = useRefWithCallback(
  node => node.addEventListener("mousedown", onMouseDown),
  node => node.removeEventListener("mousedown", onMouseDown)
);

// <div ref={setDivRef}

在上面这个例子中,setRef函数被传入了ref中,只有在DOM节点发生变化时才会调用它来更新ref,借此实现了生命周期函数。

  • 若调用时ref中已经有值,则说明组件出现了变化,先用调用上一个组件的unmount周期,再更新ref,调用其mount周期
  • 若调用时ref为null,则直接更新ref并调用其mount周期即可

最终,如果你理解了将useCallback用作元素ref的原理,你可能会根据自己的特定需求提出自己的想法。

补:在ref中使用useCallback的原理

官方文档:使用ref回调 - react.dev

众所周知,ref并非普通的props,直观地看就是我们无法从props中直接解构出ref。甚至在React.createElement中,ref参数早早地就被抽离了出来。

虽然useRef被我们当作存储视图无关变量的一般方法,但ref 本身作为props传递时却有些许不同,它可以接收三种类型的参数:

  • string ref:不推荐,已弃用
  • callback ref:将一个函数传给ref,称为回调函数,这个函数的签名为 (ref) ⇒ void
  • object ref:接收一个ref对象

这三种类型的ref具有统一的更新时机:在组件的layout阶段(或者说加载完成)时调用或赋值自己的ref参数。

标题: 【译&补】使用ref回调替代useRef吧

作者: ChlorineC

创建于: 2023-09-04 00:00:00

更新于: 2025-01-19 04:57:00

版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。

Share:

Related Posts

View All Posts »

你真的需要useMemo和useCallback吗

本文深入探讨了React中的useMemo和useCallback的使用场景和性能影响。useMemo用于缓存计算结果以减少不必要的计算,适合在计算开销较大的情况下使用,而useCallback用于缓存函数定义以避免不必要的组件刷新。作者强调,过度使用这两个Hook可能导致性能下降,建议在实际需要时再使用,并提出了其他优化方案,如使用useReducer和合理组织组件逻辑。

深入理解 React Hooks

深入理解 React Hooks

深入探讨React Hooks的设计原则、实现机制及其在函数式组件中的应用,强调声明式渲染的优势,并提供自定义Hooks的实现示例,如useAsync,用于处理异步请求和状态管理。

我修好了我的博客——如何设计一个全新的博客框架

我修好了我的博客——如何设计一个全新的博客框架

作者分享了个人博客框架迭代历程:从Hexo迁移到Astro的尝试,到发现Elog平台带来的启发,最终思考将Notion作为内容管理系统的新方案。文章重点描述了作者对博客系统的核心需求——解决创作同步问题,以及在探索NotionNext等解决方案过程中遇到的技术限制和思考。

抛弃React和Vue,让前端工程返璞归真

抛弃React和Vue,让前端工程返璞归真

本文探讨了前端开发的复杂性,提倡回归以后端为中心的架构,强调使用简单的工具和技术,如模板引擎、jQuery、alpine.js和htmx,来简化开发流程。作者认为,现代前端工具链的复杂性逐渐偏离了简化开发的初衷,建议在不引入过多复杂性的情况下,利用Web Components等标准技术来实现可复用组件,从而提高开发效率。