likes
comments
collection

React中useRef()和Refs的完整指南

作者站长头像
站长
· 阅读数 21

React中useRef()和Refs的完整指南

在这篇文章中,你将学习如何使用React.useRef() 钩子来创建持久化的可变值(也被称为引用或Refs),以及访问DOM元素。

目录

1.可变值

useRef(initialValue) 是一个内置的React钩子,它接受一个参数作为初始值并返回一个引用(又称ref)。一个引用是一个具有特殊属性的对象current

import { useRef } from 'react';
function MyComponent() {
  const reference = useRef(initialValue);
  const someHandler = () => {
    // Access reference value:
    const value = reference.current;
    // Update reference value:
    reference.current = newValue;
  };
  // ...
}

reference.current 访问引用值, 更新引用值。很简单。reference.current = newValue

React中useRef()和Refs的完整指南

关于引用,有2条规则需要记住。

  1. 引用的值在组件重新渲染时是持久的(保持不变)。
  2. 更新一个引用不会触发组件的重新渲染

现在,让我们看看如何在实践中使用useRef()

1.1 用例:记录按钮的点击情况

组件LogButtonClicks 使用一个引用来存储按钮的点击次数。

import { useRef } from 'react';
function LogButtonClicks() {
  const countRef = useRef(0);
  
  const handle = () => {
    countRef.current++;
    console.log(`Clicked ${countRef.current} times`);
  };
  console.log('I rendered!');
  return <button onClick={handle}>Click me</button>;
}

const countRef = useRef(0) 创建一个引用 countRef,初始化为 0

当按钮被点击时,handle 函数被调用,参考值被递增:countRef.current++ 。参考值被记录到控制台。

更新参考值countRef.current++ ,不会触发组件的重新渲染。这可以通过以下事实来证明:'I rendered!' 只在初始渲染时被记录到控制台,当参考值被更新时不会发生重新渲染。

现在有一个合理的问题:引用和状态之间的主要区别是什么?

引用和状态的区别

让我们重新使用上一节中的组件LogButtonClicks ,但这次使用useState() 钩子来计算点击按钮的次数。

import { useState } from 'react';
function LogButtonClicks() {
  const [count, setCount] = useState(0);
  
  const handle = () => {
    const updatedCount = count + 1;
    console.log(`Clicked ${updatedCount} times`);
    setCount(updatedCount);
  };
  console.log('I rendered!');
  return <button onClick={handle}>Click me</button>;
}

打开演示,点击按钮。每次点击,你都会在控制台中看到'I rendered!' - 意味着每次状态更新时,组件都会重新渲染。

所以,引用和状态之间的2个主要区别:

  1. 更新引用不会触发重新渲染,而更新状态会使组件重新渲染。
  2. 引用的更新是同步的(更新的引用值立即可用),而状态的更新是异步的(状态变量在重新渲染后被更新)。

从更高的角度来看,引用存储的是副作用的基础数据,而状态存储的是直接在屏幕上渲染的信息。

1.2 用例:实现一个秒表

你可以在一个引用中存储副作用的基础结构数据。例如,你可以把计时器的ID、套接字的ID等存储到引用指针中。

组件Stopwatch 使用setInterval(callback, time) 定时器函数来增加每秒的秒表计数器。计时器的ID被存储到一个引用timerIdRef :

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>Timer: {count}s</div>
      <div>
        <button onClick={startHandler}>Start</button>
        <button onClick={stopHandler}>Stop</button>
      </div>
    </div>
  );
}

startHandler() 当点击 "开始"按钮时,该函数被调用,启动定时器并将定时器ID保存在参考 timerIdRef.current = setInterval(...)

要停止秒表,用户点击 "停止"按钮。停止按钮处理程序stopHandler() ,从引用中访问定时器ID,并停止定时器clearInterval(timerIdRef.current)

此外,如果组件在秒表激活的情况下卸载,useEffect()清理功能也会停止定时器。

在秒表的例子中,引用被用来存储基础数据--活动的定时器ID。

侧面挑战:你能通过添加一个重置按钮来改进秒表吗?请在下面的评论中分享你的解决方案!

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 AccessingElement() {
  const elementRef = useRef();
   useEffect(() => {
    const divElement = elementRef.current;
    console.log(divElement); // logs <div>I'm an element</div>
  }, []);
  return (
    <div ref={elementRef}>
      I'm an element
    </div>
  );
}

2.1 用例:关注一个输入

你需要访问DOM元素,例如,在组件挂载时聚焦于输入栏。

为了让它工作,你需要创建一个对输入的引用,将该引用分配给标签的ref 属性,并在挂载后调用该元素上的特殊方法element.focus()

下面是<InputFocus> 组件的一个可能的实现。

import { useRef, useEffect } from 'react';
function InputFocus() {
  const inputRef = useRef();
  useEffect(() => {
    inputRef.current.focus();
  }, []);
  return (
    <input 
      ref={inputRef} 
      type="text" 
    />
  );
}

const inputRef = useRef() 创建一个引用来保存输入元素。

inputRef 然后被分配给输入字段的 ref属性: <input ref={inputRef} type="text" /> 。 然后,React在安装后,将inputRef.current ,作为输入元素。现在你可以通过编程将焦点设置为输入:inputRef.current.focus()

初始渲染时Ref为空

在最初的渲染过程中,应该用来保存DOM元素的引用是空的。

import { useRef, useEffect } from 'react';
function InputFocus() {
  const inputRef = useRef();
  useEffect(() => {
    // Logs `HTMLInputElement` 
    console.log(inputRef.current);
    inputRef.current.focus();
  }, []);
  // Logs `undefined` during initial rendering
  console.log(inputRef.current);
  return <input ref={inputRef} type="text" />;
}

在初始渲染期间,React仍然决定什么是组件的输出,所以还没有创建DOM结构。这就是为什么在初始渲染期间inputRef.current ,评估为undefined

useEffect(callback, []) 当输入元素已经在DOM中被创建时,钩子会在安装后立即调用回调。

callback useEffect(callback, []) 的函数是访问 inputRef.current的正确位置,因为它可以保证DOM已经被构建。

3.更新引用的限制

功能组件的函数范围应该是计算输出或调用钩子。

这就是为什么更新引用(以及更新状态)不应该在组件的函数的直接范围内进行。

引用必须在useEffect() 回调中或在处理程序(事件处理程序、定时器处理程序等)中更新。

import { useRef, useEffect } from 'react';
function MyComponent({ prop }) {
  const myRef = useRef(0);
  useEffect(() => {
    myRef.current++; // Good!
    setTimeout(() => {
      myRef.current++; // Good!
    }, 1000);
  }, []);
  const handler = () => {
    myRef.current++; // Good!
  };
  myRef.current++; // Bad!
  if (prop) {
    myRef.current++; // Bad!
  }
  return <button onClick={handler}>My button</button>;
}

4.4.总结

useRef() 钩子创建引用:

用初始值调用const reference = useRef(initialValue) ,会返回一个名为引用的特殊对象。引用对象有一个属性current :你可以使用这个属性来读取引用值reference.current ,或者更新reference.current = newValue

在组件重新渲染之间,引用的值是持久的。

更新一个引用,与更新状态相反,不会触发组件的重新渲染。

引用也可以访问DOM元素。将引用分配给你想访问的元素的ref 属性:<div ref={reference}>Element</div> - 然后该元素就可以在reference.current