React

Ref Hooks - useRef & useImperativeHandle

在这篇文章中我们将介绍 React ref 相关知识,包括forwardRef、useRef 和 useImperativeHandle
This entry is part 5 of the seriesenjoy-react-hooks

useRef

在 React 开发中,useRef 是一个非常实用的 Hook,它允许你在不触发组件重新渲染的情况下,访问和修改 DOM 元素或保存任何可变的值。这个功能对于实现一些特定的交互效果,如聚焦输入框、测量元素大小或跟踪组件内部状态而不引起更新,尤其有用。

语法

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在,即使在重新渲染时也保持不变。这意味着你可以使用它来存储任何需要跨渲染周期保留的数据,而不仅仅是对 DOM 的引用。

js
const ref = useRef(initialValue)
  • initialValueref 对象的 current 属性的初始值。可以是任意类型的值。这个参数在首次渲染后被忽略。
  • ref: 一个只有 current 属性的对象:
    • current: 初始值为传递的 initialValue。之后可以将其设置为其他值。如果将 ref 对象作为一个 JSX 节点的 ref 属性传递给 React,React 将为它设置 current 属性。

ref vs state

refstate 在 React 中都用于管理组件的数据,但它们的主要区别在于如何影响组件的渲染:

改变 ref 不会触发重新渲染

下面案例中分别创建了两个计数器: StateCounterRefCounter, 分别点击按钮并观察页面及控制台变化。

Code Playground
import React from 'react';

export function StateCounter() {
  const [count, setCount] = React.useState(0)
  const handleIncrement = () => {
    setCount(count + 1)
    console.log('State:', count)
  }

  return (
    <section>
      <button onClick={handleIncrement}>State Increment</button>
      <h2>State Count: {count}</h2>
    </section>
  )
}

export function RefCounter() {
  const countRef = React.useRef(0)

  const handleIncrement = () => {
    countRef.current++
    console.warn('Ref:', countRef.current)
  }

  return (
    <section>
      <button onClick={handleIncrement}>Ref Increment</button>
      <h2>Ref Count: {countRef.current}</h2>
    </section>
  )
}
Preview
Console

通过上面案例不难发现:

  1. 当点击StateCounter时,控制台打印 state count数值变化,同时UI更新。
  2. 当点击RefCounter时,控制台打印 ref count 数值变化,但UI不会更新。

以下是 stateref 的对比:

refstate
useRef(initialValue) 返回 { current: initialValue }useState(initialValue) 返回 state 变量的当前值和一个 state 设置函数 ( [value, setValue])
更改时不会触发重新渲染更改时触发重新渲染
可变 —— 你可以在渲染过程之外修改和更新 current 的值不可变 —— 你必须使用 state 设置函数来修改 state 变量,从而排队重新渲染
你不应在渲染期间读取(或写入) current 值你可以随时读取 state。但是,每次渲染都有自己不变的 state 快照。

通过ref操作DOM

使用 ref 操作 DOM 是非常常见的行为。React 内置了对它的支持。

  1. 首先声明一个初始值为nullref
    const inputEl = useRef(null)
  2. 然后将 ref 对象作为 ref 属性传递给想要操作的 DOM 节点的 JSX:
    <input ref={inputEl} type='text' />
  3. 当 React 创建 DOM 节点并将其渲染到屏幕时,React 将会把 DOM 节点设置为 ref 对象的 current 属性。现在可以借助 ref 对象访问 <input> 的 DOM 节点,并且可以调用类似于 focus() 的方法:
    inputEl.current.focus()
Code Playground
import { useRef } from 'react';

export default function App() {
  const inputEl = useRef(null)
  const handleFocus = () => {
    inputEl.current.focus()
  }

  return (
    <div>
      <input ref={inputEl} type='text' />
      <button onClick={handleFocus}>Focus</button>
    </div>
  )
}

useImperativeHandle

这是一个很少使用的hook, 只有当你需要 forwardRef 时, 才有可能用得到。常用于向父组件”expose“(暴露)方法。

语法

在组件顶层通过调用 useImperativeHandle 来自定义 ref 暴露出来的句柄:

import { forwardRef, useImperativeHandle } from 'react';
const CustomInput = forwardRef(function CustomInput(props, ref) {
useImperativeHandle(ref, () => {
return {
// ... 你的方法 ...
};
}, []);
// ...

用途

useImperativeHandle用于向父组件暴露暴露一个自定义的ref句柄,该句柄通常会挂载一些自定义方法。

例如: 我们定义了一个组件CustomInput, 该组件对外暴露了一个focus方法,以便调用方聚焦CustomInput。完整代码如下:

Code Playground
import { useRef,  forwardRef, useImperativeHandle } from 'react';

export default function App() {
  const inputEl = useRef(null)
  const handleFocus = () => {
    inputEl.current.focus()
  }

  return (
    <div>
      <CustomInput ref={inputEl} />
      <button onClick={handleFocus}>Focus</button>
    </div>
  )
}

// 📌 use forwardRef
const CustomInput = forwardRef(function (props, ref) {
  const inputRef = useRef(null)
  useImperativeHandle(ref, () => {
    return {
      // 📌 expose focus method to parent
      focus: () => {
        inputRef.current.focus()
      }
    };
  }, []);

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

Questions? Let's chat

discord logoOPEN DISCORD
6423
members online
previous article
Context Hooks - useContext
next article
State Hooks - useReducer

A TypeScript Full Stack Blog

Share articles about Typescript, React, Next.js, Node.js and Css from time to time.
No spam, unsubscribe at any time.