3、非受控组件实战:使用useRef获取表单值,处理文件上传等场景
好,咱们进入第三章。这一章聊点实在的——非受控组件。
说实话,我刚接触React那会儿,满脑子都是「受控组件才是正道」。每次写表单,必用useState,每个输入框都得绑个onChange。直到有一次,我接手一个老项目,里面有个超复杂的文件上传表单,光input就有二十多个,还夹杂着各种第三方UI库。改受控?改到一半我就放弃了。
后来我想明白了:不是所有场景都需要受控。有些时候,非受控组件反而更香。
3.1 什么是非受控组件?
说白了,非受控组件就是让DOM自己管自己的状态。React不插手,你通过ref去「偷看」它的值。
你想想看,传统的HTML表单怎么工作的?用户输入,浏览器存着,提交时一次性读取。非受控组件就是这个思路。
对比一下受控组件:
| 特性 | 受控组件 | 非受控组件 |
|---|---|---|
| 数据存储 | React state | DOM自身 |
| 读取时机 | 实时 | 需要时(如提交) |
| 代码量 | 较多 | 较少 |
| 适用场景 | 需要实时校验、联动 | 简单表单、文件上传 |
3.2 useRef:非受控组件的核心工具
useRef这个Hook,我愿称之为「React里的DOM操作许可证」。它返回一个可变的ref对象,这个对象的.current属性指向真实的DOM节点。
来看个最简单的例子:
import React, { useRef } from 'react';
function SimpleForm() {
const nameRef = useRef(null);
const emailRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
console.log('姓名:', nameRef.current.value);
console.log('邮箱:', emailRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>姓名:</label>
<input ref={nameRef} type="text" defaultValue="" />
</div>
<div>
<label>邮箱:</label>
<input ref={emailRef} type="email" defaultValue="" />
</div>
<button type="submit">提交</button>
</form>
);
}
注意这里我用的是defaultValue而不是value。这是非受控组件的关键——defaultValue只在初次渲染时生效,之后DOM自己管自己。
3.3 文件上传:非受控组件的王牌场景
文件上传这块,我强烈建议你用非受控组件。为什么?因为文件输入框的value是只读的,你根本没法用受控的方式去设置它。
我曾经见过有人非要用受控方式处理文件上传,结果搞了个隐藏input,再通过点击事件去触发……绕了一大圈,最后还是用ref直接读文件。
直接上代码:
import React, { useRef, useState } from 'react';
function FileUploader() {
const fileRef = useRef(null);
const [preview, setPreview] = useState(null);
const handleFileChange = () => {
const file = fileRef.current.files[0];
if (file) {
// 生成预览URL
const reader = new FileReader();
reader.onload = (e) => {
setPreview(e.target.result);
};
reader.readAsDataURL(file);
}
};
const handleUpload = async () => {
const file = fileRef.current.files[0];
if (!file) {
alert('请先选择文件');
return;
}
const formData = new FormData();
formData.append('file', file);
try {
// 模拟上传
console.log('正在上传:', file.name);
// const res = await fetch('/upload', { method: 'POST', body: formData });
console.log('上传成功');
} catch (error) {
console.error('上传失败:', error);
}
};
return (
<div>
<input
ref={fileRef}
type="file"
accept="image/*"
onChange={handleFileChange}
/>
{preview && (
<img src={preview} alt="预览" style={{ width: 200, marginTop: 10 }} />
)}
<button onClick={handleUpload}>上传</button>
</div>
);
}
fileRef.current.value = ''。
3.4 多文件上传与拖拽
实际项目中,单文件上传太少了。多数情况是批量上传,甚至带拖拽功能。
多文件上传,关键在multiple属性和files对象:
function MultiFileUploader() {
const fileRef = useRef(null);
const [fileList, setFileList] = useState([]);
const handleFilesAdded = () => {
const files = Array.from(fileRef.current.files);
setFileList(prev => [...prev, ...files]);
};
const removeFile = (index) => {
setFileList(prev => prev.filter((_, i) => i !== index));
};
return (
<div>
<input
ref={fileRef}
type="file"
multiple
onChange={handleFilesAdded}
/>
<ul>
{fileList.map((file, index) => (
<li key={index}>
{file.name} ({(file.size / 1024).toFixed(1)} KB)
<button onClick={() => removeFile(index)}>删除</button>
</li>
))}
</ul>
</div>
);
}
拖拽上传呢?我一般结合onDrop事件和ref一起用:
function DragDropUploader() {
const dropRef = useRef(null);
const [dragging, setDragging] = useState(false);
const handleDrop = (e) => {
e.preventDefault();
setDragging(false);
const files = Array.from(e.dataTransfer.files);
console.log('拖拽的文件:', files);
// 处理文件...
};
const handleDragOver = (e) => {
e.preventDefault();
setDragging(true);
};
const handleDragLeave = () => {
setDragging(false);
};
return (
<div
ref={dropRef}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
style={{
border: `2px dashed ${dragging ? '#4CAF50' : '#ccc'}`,
padding: 40,
textAlign: 'center',
transition: 'border-color 0.3s'
}}
>
{dragging ? '松开鼠标上传' : '拖拽文件到此处'}
</div>
);
}
3.5 非受控组件的表单验证
有人会问:「非受控组件怎么做实时验证?」
嗯,这个问题问得好。非受控不代表不能验证,只是验证的时机和方式不同。
我的做法是:提交时统一验证,配合onBlur做失焦验证。
function ValidatedForm() {
const nameRef = useRef(null);
const emailRef = useRef(null);
const [errors, setErrors] = useState({});
const validate = () => {
const newErrors = {};
const name = nameRef.current.value.trim();
const email = emailRef.current.value.trim();
if (!name) {
newErrors.name = '姓名不能为空';
} else if (name.length < 2) {
newErrors.name = '姓名至少2个字符';
}
if (!email) {
newErrors.email = '邮箱不能为空';
} else if (!/\S+@\S+\.\S+/.test(email)) {
newErrors.email = '邮箱格式不正确';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e) => {
e.preventDefault();
if (validate()) {
console.log('提交数据:', {
name: nameRef.current.value,
email: emailRef.current.value
});
}
};
const handleBlur = (field) => {
// 失焦时只验证当前字段
const value = field === 'name'
? nameRef.current.value
: emailRef.current.value;
if (!value.trim()) {
setErrors(prev => ({ ...prev, [field]: `${field === 'name' ? '姓名' : '邮箱'}不能为空` }));
} else {
setErrors(prev => {
const newErrors = { ...prev };
delete newErrors[field];
return newErrors;
});
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>姓名:</label>
<input
ref={nameRef}
type="text"
defaultValue=""
onBlur={() => handleBlur('name')}
/>
{errors.name && <span style={{ color: 'red' }}>{errors.name}</span>}
</div>
<div>
<label>邮箱:</label>
<input
ref={emailRef}
type="email"
defaultValue=""
onBlur={() => handleBlur('email')}
/>
{errors.email && <span style={{ color: 'red' }}>{errors.email}</span>}
</div>
<button type="submit">提交</button>
</form>
);
}
3.6 什么时候用非受控?我的经验总结
做了这么多年表单,我总结了几条经验:
- 文件上传:必须用非受控,没得选
- 简单表单:3-5个字段,没有复杂联动,非受控更清爽
- 第三方UI组件:有些库的组件不支持受控,用ref绕过去
- 性能敏感场景:超长表单(50+字段),受控组件每次setState都触发重渲染,非受控能省不少
- 表单值不需要实时响应:比如只提交不预览,非受控就够用了
但也要注意,非受控不是万能的。如果你需要:
- 实时联动(比如选择国家后自动填充城市)
- 复杂条件校验
- 表单状态需要被其他组件共享
那还是老老实实用受控组件吧。
3.7 小结
这一章我们聊了非受控组件的核心用法。useRef就像一把钥匙,让你能直接打开DOM的大门。文件上传、简单表单、拖拽交互——这些场景用非受控组件,代码量少一半,可读性还更高。
下一章,我们会深入表单验证的体系,从基础校验到自定义规则,再到异步验证。到时候你会看到,受控和非受控其实可以配合得很好。
记住一句话:工具没有好坏,只有合不合适。非受控组件不是「退步」,而是另一种选择。用对了地方,它就是利器。