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 的对比
useRef 和 useState 都是用来存储“可变对象”的容器,但它们有本质区别:
相同点
- 两者都能存储可变的值(如对象、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 state | DOM 自身 |
| 获取值方式 | 通过 state | 通过 ref.current.value |
| 是否触发渲染 | onChange 每次输入都会触发渲染 | 只在需要时读取,不触发额外渲染 |
| 适用场景 | 表单校验、实时格式化、字段联动、即时验证 | 简单表单、一次性提交、性能敏感场景、文件上传 |
| 性能 | 输入频繁时可能有轻微性能开销 | 更高(无状态更新) |
| 代码复杂度 | 较高(需要维护状态和 onChange) | 较低 |
选择建议:
- 大多数情况下优先使用受控组件,因为它符合 React “数据驱动视图”的哲学,便于调试和管理。
- 当表单非常简单、或性能要求极高、或集成第三方非 React 控件时,使用非受控组件。
- 两者可以混合使用:复杂字段用受控,简单字段用非受控。
五、总结
useRef 是 React 中一个低调却强大的 Hook,它提供了持久化的可变引用,既能访问 DOM,又能存储不触发渲染的值,是实现非受控组件的核心工具。
受控组件通过 useState 实现完整的单向数据流,适合复杂交互;非受控组件借助 useRef 更接近原生体验,适合简单场景。掌握这两者,你就能灵活应对各种表单需求。
希望本文能帮助你更好地理解 React 的表单处理机制。欢迎在评论区交流你的使用经验,或者分享你在项目中遇到的表单难题!