React useRef() 小记

418 阅读3分钟

本文主要介绍了使用React.useRef()钩子来创建持久的可变变量(也称为引用或refs)以及访问DOM元素。

1. 可变变量

useRef(initialValue)接受一个参数作为初始值,并返回一个引用,即ref,具有一个特殊属性current,ref 对象在组件的整个生命周期内保持不变。我们可以直接使用reference.current访问ref,并使用reference.current = newValue更新ref 的值,例如:

import { useRef } from 'react';

function MyComponent() {
  const reference = useRef(initialValue);
  
  const someHandler = () => {
    // 获取值
    const value = reference.current;
    
    // 更新值
    reference.current = newValue;
  };
  
  // ...
}

关于引用 ref,有两个需要注意的点:

  1. ref的值在组件重新渲染的时是保持不变的。
  2. 更新ref的值时不会触发组件重新渲染。

举个🌰:统计按钮点击次数并查看页面渲染情况。

import { useRef } from 'react';

function LogButtonClicks() {
  const countRef = useRef(0);
  
  const handle = () => {
    countRef.current++;
    console.log(`点击按钮 ${countRef.current} 次`);
  };

  console.log('我渲染了!');

  return <button onClick={handle}>点我点我</button>;
}

image.png

执行代码后,不难发现,无论点击多少次按钮,控制台始终只会打印一次'我渲染了!',这印证了上面提出的第一点。

同一个例子,使用useState() 钩子实现相同的功能。

import { useState } from 'react';

function LogButtonClicks() {
  const [count, setCount] = useState(0);
  
  const handle = () => {
    const updatedCount = count + 1;
    console.log(`点击按钮 ${updatedCount} 次`);
    setCount(updatedCount);
  };

  console.log('我渲染了!');

  return <button onClick={handle}>点我点我</button>;
}

image.png

从执行结果可以看出,每次单击时,控制台中都会打印'我渲染了!',这意味着每次更新状态时,组件都会重新渲染。因此可以得出,状态和引用之间的两个主要区别是:

  1. 更新会触发组件重新渲染,而更新引用不会。
  2. 状态更新是异步的(状态变量在重新渲染后更新),而引用是同步更新的(更新后的值立即可用)。 在实际开发过程中,状态可用于存储直接在页面上呈现的信息,引用可用于存储组件的基础结构数据,如定时器:
import { useRef, useState, useEffect } from 'react';

function StopWatch() {
  const timerIdRef = useRef(0);
  const [count, setCount] = useState(0);

  const startHandler = () => {
    if (timerIdRef.current) { 
        return; 
    }
    timerIdRef.current = setInterval(
        () => setCount(c => c+1)
    , 1000);
  };

  const stopHandler = () => {
    clearInterval(timerIdRef.current);
    timerIdRef.current = 0;
  };

  useEffect(() => {
    return () => clearInterval(timerIdRef.current);
  }, []);

  return (
    <div>
      <div>计时器: {count}s</div>
      <div>
        <button onClick={startHandler}>开始</button>
        <button onClick={stopHandler}>结束</button>
      </div>
    </div>
  );
}

在该组件中,点击“开始”按钮时将调用startHandler(),开始计数,并将计时器id保存在引用中。 当点击“停止”按钮时。执行stopHandler()停止定时并清楚计时器。此外,当卸载组件时,useEffect() return 也将停止计时器。

2.访问DOM元素

useRef()的另一个常见用途是访问DOM元素,主要分为3个步骤:

  1. 定义引用以访问元素const elementRef = useRef()
  2. 分配引用ref的元素的属性:<div ref={elementRef}></div>
  3. 挂载后,通过elementRef.current访问对应DOM元素。
import { useRef, useEffect } from 'react';

function OneElement() {
  const elementRef = useRef();

  useEffect(() => {
    const divElement = elementRef.current;
  }, []);

  return (
    <div ref={elementRef}>
      我是一个div元素
    </div>
  );
}

再举个常见🌰:自动聚焦表单

import { useRef, useEffect } from 'react';

function InputFocus() {
  const inputRef = useRef();

  useEffect(() => {
    console.log(1,inputRef.current);
    inputRef.current.focus();
  }, []);
  
  console.log(2,inputRef.current);

  return (
    <input 
      ref={inputRef} 
      type="text" 
    />
  );
}

当组件挂载完成后,通过inputRef.current获取dom元素,并调用节点方法focus()实现自动聚焦。

image.png

从控制台可以发现,在2的地方得到的节点是undefined,这是因为在初始渲染期间,页面尚未创建DOM结构,React还不能确定组件的输出是什么。而在useEffect(callback, [])中,DOM中已经创建了input元素,hook在挂载后立即调用回调:因此,callback能正常访问取值。

3. 更新引用ref值的限制

从2中举的 InputFocus可以看到,在函数组件中获取ref值或调用某个绑定元素方法时,应该需要考虑到组件生命周期,在正确的地方才能获取正确的值。不应在直接作用域内对ref进行操作(包括更新state),必须在useEffect()回调内部某个方法事件内部进行操作。

import { useRef, useEffect } from 'react';

function MyComponent({ props }) {
  const myRef = useRef(0);

  useEffect(() => {
    myRef.current++; // Good!

    setTimeout(() => {
      myRef.current++; // Good!
    }, 1000);
  }, []);

  const handler = () => {
    myRef.current++; // Good!
  };

  myRef.current++; // Bad!

  if (props) {
    myRef.current++; // Bad!
  }

  return <button onClick={handler}>点我点我</button>;
}