精读React hooks(四):useRef的多维用途

🧑‍💻
推荐全栈学习资源:
  • Next.js 中文文档:样式和官网一样的中文文档,创造沉浸式Next.js中文学习体验。
  • 《Chrome插件全栈开发》:真实出海项目的实战教学课,讲解Chrome插件和Next.js端的全栈开发,帮助你半个月内成为全栈出海工程师。
  • 在 React 里,我们经常听到 "everything is a component" 这样的说法。而为了保持组件的纯净性,React 强调声明式编程,减少直接操作 DOM 的情况。然而,有时我们仍然需要直接与 DOM 交互,或者访问某个组件的具体实例。在这些情况下,Refs 就派上用场了。

    useRef 基础知识

    // 定义
    const inputRef = useRef(null);
     
    // 使用
    console.log(inputRef.current)

    这是useRef的使用示例,useRef返回一个可变的 ref 对象,通过.current可以获取保存在useRef的值。看起来像是一个复杂版的useState,那么useStateuseRef有什么区别?为什么需要useRef呢?

    主要原因有两个:

    1. 持久性useRef的返回对象在组件的整个生命周期中都是持久的,而不是每次渲染都重新创建。
    2. 不会触发渲染:当useState中的状态改变时,组件会重新渲染。而当useRef.current属性改变时,组件不会重新渲染。

    总结来说,useRef既能保存状态,还不会在更新时触发渲染。本文我们就来盘点一下useRef的使用场景。

    useRef 的常见用途

    访问 DOM 元素

    当我们需要直接与 DOM 元素进行交互(例如,手动获取焦点或测量元素尺寸)时,可以使用 useRef

    function TextInput() {
      const inputRef = useRef(null);
     
      function focusInput() {
        inputRef.current.focus();
      }
     
      return (
        <div>
          <input ref={inputRef} type="text" />
          <button onClick={focusInput}>Focus the input</button>
        </div>
      );
    }

    我们还可以在组件嵌套的场景使用useRef

    import { forwardRef, useRef } from 'react';
     
    const MyInput = forwardRef((props, ref) => {
      return <input {...props} ref={ref} />;
    });
     
    export default function Form() {
      const inputRef = useRef(null);
     
      function handleClick() {
        inputRef.current.focus();
      }
     
      return (
        <>
          <MyInput ref={inputRef} />
          <button onClick={handleClick}>
            Focus the input
          </button>
        </>
      );
    }

    保存状态但不触发渲染

    有时,你可能需要在组件中保存某些值,而不希望每次该值更改时都重新渲染组件。在这种情况下,useRef很有用。

    function Timer() {
      const count = useRef(0);
     
      useEffect(() => {
        const intervalId = setInterval(() => {
          count.current += 1;
          console.log(`Elapsed time: ${count.current} seconds`);
        }, 1000);
     
        return () => clearInterval(intervalId);
      }, []);
     
      return <div>Check the console to see the elapsed time!</div>;
    }

    这个示例完美说明了可以把useRef视为一个能够在组件的整个生命周期中持久保存数据的“盒子”,而不会引起组件的重新渲染。

    保存上一次的 props 或 state

    在某些情况下,你可能需要知道 props 或 state 的上一次值。这时可以使用useRef结合useEffect来达到目的。

    function DisplayValue({ value }) {
    	const [prevValue, setPrevValue] = useState(null); // 初始时,没有前一个值
      const previousValue = useRef(value);
     
      useEffect(() => {
    		setPrevValue(currentRef.current);
        previousValue.current = value;
      }, [value]);
     
      return (
        <div>
          Current Value: {value} <br />
          Previous Value: {prevValue}
        </div>
      );
    }

    当组件首次渲染时,previousValue.current会被初始化为value的当前值。随后,每当value发生变化时,useEffect都会运行并更新previousValue.current为新的value

    但这里有一个微妙之处:由于useEffect是在组件渲染之后运行的,因此在组件的渲染过程中,previousValue.current的值是从前一次渲染中保持不变的。只有当useEffect被调用并执行完毕后,previousValue.current才会更新为新的value

    高级技巧

    避免在渲染期间读/写 ref

    function DisplayValue({ value }) {
      const previousValue = useRef(value);
     
    	// 错误:在渲染期间修改 ref
      if (previousValue.current !== value) {
        previousValue.current = value;
      }
     
      return (
        <div>
          Current Value: {value} <br />
    			 {/* 错误:在渲染期间读 ref */}
          Previous Value: {previousValue.current}
        </div>
      );
    }

    这里,我们尝试在组件的渲染期间更新previousValue.current。这违反了 React 的工作方式,并可能导致不可预测的行为。例如:

    1. 不稳定的 UI:由于 React 在多次渲染中可能使用异步和优化技术,直接在渲染期间修改 refs 可能导致 UI 不一致。
    2. 依赖更新:如果其他效应或钩子依赖于 ref 的值,它们可能不会在期望的时刻运行,因为直接修改 ref 不会触发重新渲染或其他效应。

    这是为什么我们通常在useEffect内部更新 refs。在useEffect内部,我们可以确保组件已经完成渲染,并且不会在渲染期间发生任何不期望的副作用。

    避免重复创建 ref

    如果我们在创建 ref 时,想要通过计算或有副作用的方法获取初值,可能会用下面这种写法。这种写法会导致getInitialCount()在每次组建渲染的时候都被调用。虽然useRef的设计让它只从首次渲染的时候获取初值,但这种做法仍然会造成不必要的性能损耗。

    function ClickCounter() {
      // bad。这里的问题是,每次组件渲染时,getInitialCount都会被调用,尽管它的返回值只在第一次渲染时被使用。
      const countRef = useRef(getInitialCount());
      
      function handleClick() {
        countRef.current += 1;
        console.log(`Button clicked ${countRef.current} times.`);
      }
     
      return <button onClick={handleClick}>Click me!</button>;
    }

    解决这种场景下的 ref 创建也很简单,那就是用null作为初始值,渲染的过程判断仅在null时去计算或调用有副作用的方法。

    function ClickCounter() {
    	// good
      const countRef = useRef(null);
    	// good
      if (countRef.current === null) {
        countRef.current = getInitialCount();
      }
     
      function handleClick() {
        countRef.current += 1;
        console.log(`Button clicked ${countRef.current} times.`);
      }
     
      return <button onClick={handleClick}>Click me!</button>;
    }

    与 useReducer 使用

    当我们需要复杂的状态逻辑且希望避免额外的渲染时,可以考虑将useRefuseReducer结合使用。

    例如:跟踪useReducer的 action 数量。

    const initialState = { count: 0 };
    function reducer(state, action) {
      switch (action.type) {
        case "increment":
          return { count: state.count + 1 };
        default:
          throw new Error();
      }
    }
     
    function Counter() {
      const [state, dispatch] = useReducer(reducer, initialState);
      const actionsCountRef = useRef(0);
     
      function handleIncrement() {
        dispatch({ type: "increment" });
        actionsCountRef.current += 1;
        console.log(`Actions count: ${actionsCountRef.current}`);
      }
     
      return (
        <>
          Count: {state.count}
          <button onClick={handleIncrement}>Increment</button>
        </>
      );
    }

    与第三方库集成

    在使用非 React 库(如 D3、jQuery)时,我们可能需要使用useRef来获得对真实 DOM 节点的引用。

    例如:结合D3

    import { useRef, useEffect } from 'react';
    import * as d3 from 'd3';
     
    function BarChart() {
      const chartRef = useRef(null);
     
      useEffect(() => {
        const svg = d3.select(chartRef.current);
        // ... 使用 D3 进行图表绘制
      }, []);
     
      return <svg ref={chartRef}></svg>;
    }

    动画处理

    通过useRef获取元素并使用 Web API 如requestAnimationFrame可以实现复杂的动画效果。

    import { useEffect, useRef } from "react";
     
    function MovingBox() {
      const boxRef = useRef(null);
      const animationFrameRef = useRef(null);
     
      useEffect(() => {
        const boxElem = boxRef.current;
        let position = 0;
     
        const animate = () => {
          position += 1;
          if (position > window.innerWidth) {
            position = -100; // 如果方块移动到屏幕的右侧,则从左侧重新开始
          }
          boxElem.style.transform = `translateX(${position}px)`;
          animationFrameRef.current = requestAnimationFrame(animate);
        };
     
        animationFrameRef.current = requestAnimationFrame(animate);
     
        return () => {
          cancelAnimationFrame(animationFrameRef.current); // 在组件卸载时取消动画
        };
      }, []);
     
      return (
        <div
          ref={boxRef}
          style={{ width: "100px", height: "100px", background: "blue" }}
        ></div>
      );
    }
     
    export default MovingBox;

    事件监听

    使用useRef监听不由 React 管理的 DOM 事件。

    例如:窗口大小变化

    function WindowSize() {
      const widthRef = useRef(window.innerWidth);
      
      useEffect(() => {
        const handleResize = () => {
          widthRef.current = window.innerWidth;
          console.log(`Width: ${widthRef.current}`);
        };
        
        window.addEventListener("resize", handleResize);
        return () => window.removeEventListener("resize", handleResize);
      }, []);
      
      return <div>Check the console for window width updates!</div>;
    }

    结语

    在本篇文章中,我们从基本的 DOM 引用出发,探讨了各种实际的应用场景,包括性能优化和动画方面。通过深入了解并有效使用 useRef,我们可以更灵活地管理组件内部的状态,而不必担心触发不必要的渲染。希望这篇文章能帮助你更好地理解useRef并能让你有所启发。

    以上多个重要示例的实际效果都可以在我的示例站查看,TypeScript版的源码也已发布到我的Github:useRef分支

    专栏资源

    专栏博客地址:精读React Hooks

    专栏演示站:React Hooks Demos

    专栏源码仓库:👉Github - Source Code

    交个朋友:👉加入「独立全栈交流群」

    专栏文章列表:

    精读React hooks(一):useState 的几个基础用法和进阶技巧

    精读React hooks(二):React状态管理的强大工具——useReducer

    精读React hooks(三):useContext从基础应用到性能优化

    精读React hooks(四):useRef的多维用途

    精读React hooks(五):useEffect使用细节知多少?

    精读React hooks(六):useLayoutEffect解决了什么问题?

    精读React hooks(七):用useMemo来减少性能开销

    精读React hooks(八):我们为什么需要useCallback

    精读React hooks(九):使用useTransition进行非阻塞渲染

    精读React hooks(十):使用useDeferredValue延迟状态更新

    精读React hooks(十一):useInsertionEffect——CSS-in-JS样式注入新方式

    精读React hooks(十二):使用useImperativeHandle能获得什么能力

    精读React hooks(十三):使用useSyncExternalStore获取实时数据

    精读React hooks(十四):总有一天你会需要useId为你生成唯一id

    精读React hooks(十五):把useDebugValue加入你的React调试工具库

    精读React hooks(十六):一个为代码优雅而生的hook——use