#说说对受控组件和非受控组件的理解?应用场景?
#一、是什么
在 React 中处理表单元素时,根据数据管理方式的不同,组件可以分为两类:
- 受控组件(Controlled Component):表单元素的值由 React 状态驱动,每次变化都通过事件处理函数更新状态,React 是数据的"唯一真相源"
- 非受控组件(Uncontrolled Component):表单元素的值由 DOM 自身管理,React 通过
ref在需要时读取值,DOM 是数据的"真相源"
这两种模式各有优劣,适用于不同的场景。
#二、受控组件
受控组件的核心特征是:表单元素的 value 属性绑定到 React 的 state,通过 onChange 事件同步更新。
#基础用法
import { useState } from 'react';
function LoginForm() {
const [formData, setFormData] = useState({
username: '',
password: '',
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log('提交数据:', formData);
};
return (
<form onSubmit={handleSubmit}>
<input
name="username"
value={formData.username}
onChange={handleChange}
placeholder="用户名"
/>
<input
name="password"
type="password"
value={formData.password}
onChange={handleChange}
placeholder="密码"
/>
<button type="submit">登录</button>
</form>
);
}#受控组件的实时校验
受控组件的一大优势是可以在输入过程中实时处理和校验数据:
function RegistrationForm() {
const [email, setEmail] = useState('');
const [emailError, setEmailError] = useState('');
const validateEmail = (value: string) => {
if (!value) {
setEmailError('邮箱不能为空');
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
setEmailError('邮箱格式不正确');
} else {
setEmailError('');
}
};
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setEmail(value);
validateEmail(value);
};
return (
<div>
<input
type="email"
value={email}
onChange={handleEmailChange}
className={emailError ? 'input-error' : ''}
/>
{emailError && <span className="error">{emailError}</span>}
</div>
);
}#受控组件的输入格式化
受控组件可以轻松实现输入格式化,比如电话号码的自动格式化:
function PhoneInput() {
const [phone, setPhone] = useState('');
const formatPhone = (value: string): string => {
const digits = value.replace(/\D/g, '').slice(0, 11);
if (digits.length <= 3) return digits;
if (digits.length <= 7) return `${digits.slice(0, 3)}-${digits.slice(3)}`;
return `${digits.slice(0, 3)}-${digits.slice(3, 7)}-${digits.slice(7)}`;
};
return (
<input
value={phone}
onChange={e => setPhone(formatPhone(e.target.value))}
placeholder="请输入手机号"
/>
);
}#各种表单元素的受控写法
function FormExample() {
const [text, setText] = useState('');
const [selected, setSelected] = useState('option1');
const [checked, setChecked] = useState(false);
const [multiSelected, setMultiSelected] = useState<string[]>([]);
return (
<form>
{/* 文本输入 */}
<input value={text} onChange={e => setText(e.target.value)} />
{/* 下拉选择 */}
<select value={selected} onChange={e => setSelected(e.target.value)}>
<option value="option1">选项一</option>
<option value="option2">选项二</option>
<option value="option3">选项三</option>
</select>
{/* 复选框 */}
<label>
<input
type="checkbox"
checked={checked}
onChange={e => setChecked(e.target.checked)}
/>
同意条款
</label>
{/* 文本域 */}
<textarea value={text} onChange={e => setText(e.target.value)} />
</form>
);
}#三、非受控组件
非受控组件将数据存储在 DOM 中,通过 ref 访问表单值。使用 defaultValue(而非 value)设置初始值。
#基础用法
import { useRef } from 'react';
function UncontrolledForm() {
const usernameRef = useRef<HTMLInputElement>(null);
const passwordRef = useRef<HTMLInputElement>(null);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const username = usernameRef.current?.value;
const password = passwordRef.current?.value;
console.log('提交数据:', { username, password });
};
return (
<form onSubmit={handleSubmit}>
<input ref={usernameRef} defaultValue="" placeholder="用户名" />
<input ref={passwordRef} type="password" defaultValue="" placeholder="密码" />
<button type="submit">登录</button>
</form>
);
}#文件上传——天然的非受控组件
<input type="file" /> 是一个典型的非受控组件,因为它的值只能由用户设置,不能通过程序设定:
function FileUpload() {
const fileRef = useRef<HTMLInputElement>(null);
const handleUpload = async () => {
const files = fileRef.current?.files;
if (!files || files.length === 0) return;
const formData = new FormData();
Array.from(files).forEach(file => {
formData.append('files', file);
});
await fetch('/api/upload', {
method: 'POST',
body: formData,
});
};
return (
<div>
<input ref={fileRef} type="file" multiple accept="image/*" />
<button onClick={handleUpload}>上传</button>
</div>
);
}#defaultValue vs value
function DefaultValueExample() {
return (
<div>
{/* ✅ 非受控:设置初始值,之后 DOM 自行管理 */}
<input defaultValue="初始文本" />
{/* ❌ 受控但没有 onChange:输入框会变成只读 */}
{/* React 会发出警告 */}
{/* <input value="固定文本" /> */}
{/* ✅ 如果确实需要只读的受控输入 */}
<input value="只读文本" readOnly />
</div>
);
}#四、React 19 中的表单处理
React 19 引入了 Form Actions,提供了一种新的表单处理方式:
import { useActionState } from 'react';
async function submitForm(prevState: any, formData: FormData) {
const username = formData.get('username') as string;
const password = formData.get('password') as string;
if (!username || !password) {
return { error: '用户名和密码不能为空' };
}
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ username, password }),
headers: { 'Content-Type': 'application/json' },
});
if (!res.ok) {
return { error: '登录失败' };
}
return { success: true };
}
function LoginForm() {
const [state, formAction, isPending] = useActionState(submitForm, null);
return (
<form action={formAction}>
<input name="username" placeholder="用户名" required />
<input name="password" type="password" placeholder="密码" required />
{state?.error && <p className="error">{state.error}</p>}
<button type="submit" disabled={isPending}>
{isPending ? '登录中...' : '登录'}
</button>
</form>
);
}这种方式结合了非受控组件的简洁性和服务端交互的便利性,特别适合与 Server Components 配合使用。
#五、useFormStatus
React 19 还提供了 useFormStatus Hook,用于获取表单提交状态:
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? '提交中...' : '提交'}
</button>
);
}
function ContactForm() {
async function handleSubmit(formData: FormData) {
'use server';
const name = formData.get('name');
const message = formData.get('message');
await saveMessage({ name, message });
}
return (
<form action={handleSubmit}>
<input name="name" placeholder="姓名" />
<textarea name="message" placeholder="留言内容" />
<SubmitButton />
</form>
);
}#六、对比总结
| 特性 | 受控组件 | 非受控组件 |
|---|---|---|
| 数据管理 | React state | DOM |
| 取值方式 | 直接读取 state | 通过 ref 读取 |
| 初始值设置 | value + onChange | defaultValue |
| 实时校验 | ✅ 方便 | ❌ 困难 |
| 输入格式化 | ✅ 方便 | ❌ 困难 |
| 动态禁用提交 | ✅ 方便 | ❌ 需额外逻辑 |
| 性能(大表单) | 每次输入触发渲染 | 无额外渲染 |
| 代码量 | 较多 | 较少 |
| 测试便利性 | ✅ 状态可预测 | 需要模拟 DOM |
#七、应用场景建议
#适合受控组件的场景
- 需要实时校验的表单(注册、登录)
- 需要输入格式化(手机号、银行卡号)
- 需要根据输入联动其他 UI(搜索建议、动态表单)
- 需要程序化控制表单值(重置、填充默认值)
#适合非受控组件的场景
- 简单的一次性表单(搜索框、评论输入)
- 文件上传
- 需要集成第三方 DOM 库
- 大型表单追求极致性能(配合表单库如 React Hook Form)
// React Hook Form 采用非受控 + ref 的方式,性能优异
import { useForm } from 'react-hook-form';
interface FormValues {
username: string;
email: string;
age: number;
}
function PerformantForm() {
const { register, handleSubmit, formState: { errors } } = useForm<FormValues>();
const onSubmit = (data: FormValues) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('username', { required: '用户名必填' })} />
{errors.username && <span>{errors.username.message}</span>}
<input {...register('email', {
required: '邮箱必填',
pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: '邮箱格式错误' }
})} />
{errors.email && <span>{errors.email.message}</span>}
<input type="number" {...register('age', { min: { value: 0, message: '年龄不能为负' } })} />
{errors.age && <span>{errors.age.message}</span>}
<button type="submit">提交</button>
</form>
);
}#八、总结
受控组件和非受控组件代表了 React 中管理表单数据的两种哲学。受控组件让 React 完全掌控数据,适合需要实时交互的场景;非受控组件让 DOM 管理数据,适合简单表单和性能敏感场景。React 19 的 Form Actions 则提供了第三种选择,结合了两者的优点。在实际项目中,应根据具体需求灵活选择,甚至在同一个表单中混合使用。