useRef 不只是拿 DOM!手把手教你玩转 React 受控与非受控组件

34 阅读6分钟

useRef 不只是拿 DOM!手把手教你玩转 React 受控与非受控组件

今天想和大家聊聊 React 中一个非常实用的 Hook —— useRef,以及它在表单处理中的核心应用:受控组件和非受控组件。这两个概念是 React 表单开发的基石,理解它们能让你在处理用户输入时游刃有余。

本文将从 useRef 的基础概念入手,对比它与 useState 的异同,探讨其常见应用场景,然后深入讲解受控组件和非受控组件的实现方式、区别以及适用场景。文章配有完整代码示例,适合 React 初学者和进阶开发者阅读。让我们开始吧!

一、useRef Hook 基础概念

useRef 是 React 提供的一个 Hook,用于创建一个可变的引用对象。这个对象在组件的整个生命周期中持久存在,不会因为组件重新渲染而丢失。

与 Vue 的对比

如果你有 Vue 经验,会发现 useRef 结合 useEffect(依赖数组为空时)非常类似于 Vue 的 onMounted 生命周期。在组件挂载后,我们常常需要操作 DOM 元素,这时候 useRef 就派上用场了。

如何获取 DOM 元素?

在 React 中,直接操作 DOM 不是推荐做法,但有时不可避免(如聚焦输入框、第三方库集成)。useRef 正是为此设计的:

  • 创建引用:const inputRef = useRef(null);
  • 绑定到元素:<input ref={inputRef} />
  • 获取 DOM:inputRef.current(就是一个原生 DOM 节点)

useRef 与 useState 的对比

useRefuseState 都是用来存储“可变对象”的容器,但它们有本质区别:

相同点
  • 两者都能存储可变的值(如对象、DOM 节点等)。
  • useState 的状态变化会触发重新渲染。
  • useRef 存储的值也可以变化,但不会触发渲染。
不同点
  • 响应式useState 是响应式的,值变化会导致组件重新渲染;useRef 不是响应式的,改变 .current 值不会引起渲染。
  • 使用场景useState 适合需要驱动 UI 更新的状态;useRef 适合“默默存储”一些不需要触发渲染的值。

简单来说,useRef 是一个“不会引起重新渲染的可变容器”,非常适合存储那些“幕后工作者”。

二、useRef 的应用场景

1. DOM 节点的引用

最经典的用法:获取 DOM 元素并操作它。

import { useState, useRef, useEffect } from 'react';

export default function App() {
  const [count, setCount] = useState(0);
  const inputRef = useRef(null);

  useEffect(() => {
    // 组件挂载后自动聚焦
    inputRef.current.focus();
  }, []); // 空依赖数组,类似于 onMounted

  return (
    <>
      <input ref={inputRef} />
      {count}
      <button onClick={() => setCount(count + 1)}>count ++</button>
    </>
  );
}

在这个例子中,组件挂载后输入框自动获得焦点。即使点击按钮触发 count 变化重新渲染,inputRef.current 依然指向同一个 DOM 节点。

2. 存储可变值(不触发渲染)

另一个常见场景:存储定时器 ID、上一轮状态等不需要引起渲染的值。

import { useRef, useState } from 'react';

export default function App() {
  const intervalRef = useRef(null);
  const [count, setCount] = useState(0);

  const start = () => {
    intervalRef.current = setInterval(() => {
      console.log('tick~~~~');
    }, 1000);
  };

  const stop = () => {
    clearInterval(intervalRef.current);
  };

  return (
    <>
      <button onClick={start}>开始</button>
      <button onClick={stop}>停止</button>
      {count}
      <button onClick={() => setCount(count + 1)}>count ++</button>
    </>
  );
}

这里 intervalRef 用来持久化存储定时器 ID。即使组件多次渲染,intervalRef.current 始终保持最新值,且修改它不会导致不必要的重新渲染。

3. 创建持久化的引用对象

useRef 返回的对象 { current: initialValue } 在组件整个生命周期内只有一个实例。这使得它成为存储“跨渲染周期共享数据”的理想选择。

三、受控组件与非受控组件

表单是前端开发中最常见的交互场景。React 提供了两种处理表单的方式:受控组件非受控组件

核心问题:如何获取用户在表单中的输入值?

受控组件(Controlled Components)

受控组件的核心思想:表单元素的值由 React 状态(state)完全控制

  • 通过 value 属性绑定状态。
  • 通过 onChange 事件实时更新状态。
  • 实现“单向数据流”:状态 → UI,用户输入 → 更新状态 → 重新渲染 UI。
优点
  • 状态统一管理,便于表单校验、字段联动、实时格式化。
  • 适合复杂表单(如即时验证、条件渲染)。
示例:混合展示受控与非受控
import { useState, useRef } from 'react';

export default function App() {
  const [value, setValue] = useState('');
  const inputRef = useRef(null);

  const doLogin = (e) => {
    e.preventDefault();
    console.log('非受控输入值:', inputRef.current.value);
    console.log('受控输入值:', value);
  };

  return (
    <form onSubmit={doLogin}>
      <p>实时显示受控值:{value}</p>
      <input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
      <input type="text" ref={inputRef} placeholder="非受控输入" />
      <button type="submit">登录</button>
    </form>
  );
}

第一个输入框是受控的:值由 value 状态控制,用户输入会实时更新状态并显示在页面上。第二个是非受控的,仅在提交时读取。

多字段表单示例(受控)
import { useState } from 'react';

export default function LoginForm() {
  const [form, setForm] = useState({
    username: '',
    password: ''
  });

  const handleChange = (e) => {
    setForm({
      ...form,
      [e.target.name]: e.target.value
    });
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('表单数据:', form);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        placeholder="请输入用户名"
        name="username"
        value={form.username}
        onChange={handleChange}
      />
      <input
        type="password"
        placeholder="请输入密码"
        name="password"
        value={form.password}
        onChange={handleChange}
      />
      <button type="submit">注册</button>
    </form>
  );
}

这种方式非常适合需要实时校验、字段联动或条件显示的复杂表单。

非受控组件(Uncontrolled Components)

非受控组件的核心思想:表单元素的值由 DOM 自身管理,React 只在需要时(如提交)通过 ref 获取值。

  • 使用 useRef 绑定元素。
  • 通过 ref.current.value 读取值。
优点
  • 更接近原生 HTML 行为。
  • 性能更好(避免频繁渲染)。
  • 适合简单表单、一次性读取场景(如文件上传、第三方表单库)。
示例:评论框(非受控)
import { useRef } from 'react';

export default function CommentBox() {
  const textareaRef = useRef(null);

  const handleSubmit = () => {
    const comment = textareaRef.current.value.trim();
    if (!comment) return alert('请输入评论');
    console.log('提交评论:', comment);
    // 可选:清空
    textareaRef.current.value = '';
  };

  return (
    <div>
      <textarea
        ref={textareaRef}
        placeholder="输入评论。。。"
      />
      <button onClick={handleSubmit}>提交</button>
    </div>
  );
}

提交时直接读取 DOM 值,无需维护状态,代码更简洁。

四、受控 vs 非受控:如何选择?

特性受控组件非受控组件
值来源React stateDOM 自身
获取值方式通过 state通过 ref.current.value
是否触发渲染onChange 每次输入都会触发渲染只在需要时读取,不触发额外渲染
适用场景表单校验、实时格式化、字段联动、即时验证简单表单、一次性提交、性能敏感场景、文件上传
性能输入频繁时可能有轻微性能开销更高(无状态更新)
代码复杂度较高(需要维护状态和 onChange)较低

选择建议

  • 大多数情况下优先使用受控组件,因为它符合 React “数据驱动视图”的哲学,便于调试和管理。
  • 当表单非常简单、或性能要求极高、或集成第三方非 React 控件时,使用非受控组件
  • 两者可以混合使用:复杂字段用受控,简单字段用非受控。

五、总结

useRef 是 React 中一个低调却强大的 Hook,它提供了持久化的可变引用,既能访问 DOM,又能存储不触发渲染的值,是实现非受控组件的核心工具。

受控组件通过 useState 实现完整的单向数据流,适合复杂交互;非受控组件借助 useRef 更接近原生体验,适合简单场景。掌握这两者,你就能灵活应对各种表单需求。

希望本文能帮助你更好地理解 React 的表单处理机制。欢迎在评论区交流你的使用经验,或者分享你在项目中遇到的表单难题!