pin_drop当前位置:知识文库 ❯ 图文
框架集成:06 React表单与HTML - 详细教程与实战指南
教程简介
表单是Web应用中最常见的交互方式,React对HTML表单的处理有着独特的设计理念。在React中,表单元素分为受控组件和非受控组件两大类。受控组件将表单值与React状态绑定,通过状态驱动UI更新;非受控组件则通过ref直接访问DOM获取表单值。
本教程将详细介绍受控组件与非受控组件、ref获取表单值、表单验证、多表单处理以及常用表单库(Formik、React Hook Form)的集成方式。
核心概念
受控组件
受控组件是React推荐的表单处理方式,表单值由React状态完全控制。以下是一个完整的受控组件注册表单示例:
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>React受控组件</title>
<style>
body { font-family: -apple-system, sans-serif; padding: 20px; max-width: 900px; margin: 0 auto; background: #f5f5f5; }
.panel { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); margin: 15px 0; }
pre { background: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 8px; overflow-x: auto; font-size: 13px; }
.form-group { margin-bottom: 15px; }
.form-group label { display: block; margin-bottom: 5px; font-weight: bold; color: #333; }
.form-group input, .form-group select, .form-group textarea {
width: 100%; padding: 10px 12px; border: 2px solid #e0e0e0; border-radius: 6px;
font-size: 14px; transition: border-color 0.2s; box-sizing: border-box;
}
.form-group input:focus, .form-group select:focus, .form-group textarea:focus {
outline: none; border-color: #667eea;
}
.form-group .error { color: #e74c3c; font-size: 13px; margin-top: 4px; }
.form-group .hint { color: #999; font-size: 12px; margin-top: 4px; }
.checkbox-group { display: flex; align-items: center; gap: 8px; }
.checkbox-group input { width: auto; }
.radio-group { display: flex; gap: 15px; }
.radio-group label { display: flex; align-items: center; gap: 5px; font-weight: normal; cursor: pointer; }
button { padding: 10px 24px; border: none; border-radius: 6px; cursor: pointer; font-size: 15px; }
.btn-primary { background: #667eea; color: white; }
.btn-primary:hover { background: #5a6fd6; }
.btn-primary:disabled { background: #ccc; cursor: not-allowed; }
.preview { background: #f8f9fa; padding: 15px; border-radius: 8px; border: 1px solid #e0e0e0; margin-top: 15px; }
.preview h4 { margin-top: 0; color: #667eea; }
</style>
</head>
<body>
<h1>React受控组件</h1>
<div id="controlled-demo"></div>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script crossorigin src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
function ControlledForm() {
const [formData, setFormData] = React.useState({
username: '',
email: '',
password: '',
gender: '',
bio: '',
subscribe: false,
level: 'beginner'
});
const [errors, setErrors] = React.useState({});
function handleChange(e) {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
}
function validate() {
const newErrors = {};
if (!formData.username.trim()) newErrors.username = '用户名不能为空';
else if (formData.username.length < 3) newErrors.username = '用户名至少3个字符';
if (!formData.email.trim()) newErrors.email = '邮箱不能为空';
else if (!/\S+@\S+\.\S+/.test(formData.email)) newErrors.email = '邮箱格式不正确';
if (!formData.password) newErrors.password = '密码不能为空';
else if (formData.password.length < 6) newErrors.password = '密码至少6个字符';
if (!formData.gender) newErrors.gender = '请选择性别';
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
}
function handleSubmit(e) {
e.preventDefault();
if (validate()) {
alert('表单提交成功!\n' + JSON.stringify(formData, null, 2));
}
}
return (
<div>
<div className="panel">
<h3>受控组件 - 注册表单</h3>
<p style={{ color: '#666', fontSize: '14px' }}>每个表单值都由React状态控制,通过onChange实时更新</p>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>用户名</label>
<input type="text" name="username" value={formData.username} onChange={handleChange} placeholder="请输入用户名" />
{errors.username && <div className="error">{errors.username}</div>}
</div>
<div className="form-group">
<label>邮箱</label>
<input type="email" name="email" value={formData.email} onChange={handleChange} placeholder="请输入邮箱" />
{errors.email && <div className="error">{errors.email}</div>}
</div>
<div className="form-group">
<label>密码</label>
<input type="password" name="password" value={formData.password} onChange={handleChange} placeholder="请输入密码" />
{errors.password && <div className="error">{errors.password}</div>}
<div className="hint">密码长度至少6位</div>
</div>
<div className="form-group">
<label>性别</label>
<div className="radio-group">
<label><input type="radio" name="gender" value="male" checked={formData.gender === 'male'} onChange={handleChange} /> 男</label>
<label><input type="radio" name="gender" value="female" checked={formData.gender === 'female'} onChange={handleChange} /> 女</label>
</div>
{errors.gender && <div className="error">{errors.gender}</div>}
</div>
<div className="form-group">
<label>等级</label>
<select name="level" value={formData.level} onChange={handleChange}>
<option value="beginner">初学者</option>
<option value="intermediate">中级</option>
<option value="advanced">高级</option>
</select>
</div>
<div className="form-group">
<label>个人简介</label>
<textarea name="bio" value={formData.bio} onChange={handleChange} rows="3" placeholder="介绍一下你自己" />
</div>
<div className="form-group">
<div className="checkbox-group">
<input type="checkbox" name="subscribe" checked={formData.subscribe} onChange={handleChange} id="subscribe" />
<label htmlFor="subscribe" style={{ fontWeight: 'normal' }}>订阅新闻通讯</label>
</div>
</div>
<button type="submit" className="btn-primary">提交注册</button>
</form>
</div>
<div className="panel">
<div className="preview">
<h4>实时状态预览</h4>
<pre style={{ background: '#2d2d2d', color: '#f8f8f2', padding: '10px', borderRadius: '6px', fontSize: '12px' }}>
{JSON.stringify(formData, null, 2)}
</pre>
</div>
</div>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('controlled-demo'));
root.render(<ControlledForm />);
</script>
</body>
</html>非受控组件与ref
非受控组件通过ref直接访问DOM,表单值由DOM自身管理。适用于文件输入等需要直接操作DOM的场景:
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>非受控组件与ref</title>
<style>
body { font-family: -apple-system, sans-serif; padding: 20px; max-width: 900px; margin: 0 auto; background: #f5f5f5; }
.panel { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); margin: 15px 0; }
pre { background: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 8px; overflow-x: auto; font-size: 13px; }
.form-group { margin-bottom: 15px; }
.form-group label { display: block; margin-bottom: 5px; font-weight: bold; }
.form-group input { width: 100%; padding: 10px; border: 2px solid #e0e0e0; border-radius: 6px; font-size: 14px; box-sizing: border-box; }
.form-group input:focus { outline: none; border-color: #667eea; }
button { padding: 10px 24px; border: none; border-radius: 6px; cursor: pointer; font-size: 15px; margin: 5px; }
.btn-primary { background: #667eea; color: white; }
.btn-secondary { background: #e0e0e0; color: #333; }
.comparison { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
.comp-card { padding: 15px; border: 1px solid #e0e0e0; border-radius: 8px; }
.comp-card h4 { margin-top: 0; }
.controlled { border-color: #27ae60; background: #f0faf5; }
.uncontrolled { border-color: #f39c12; background: #fffaf0; }
.file-preview { margin-top: 10px; }
.file-preview img { max-width: 200px; max-height: 150px; border-radius: 6px; }
</style>
</head>
<body>
<h1>非受控组件与ref</h1>
<div id="uncontrolled-demo"></div>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script crossorigin src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
function UncontrolledForm() {
const nameRef = React.useRef(null);
const emailRef = React.useRef(null);
const fileRef = React.useRef(null);
const [filePreview, setFilePreview] = React.useState(null);
const [submittedData, setSubmittedData] = React.useState(null);
function handleSubmit(e) {
e.preventDefault();
setSubmittedData({
name: nameRef.current.value,
email: emailRef.current.value,
file: fileRef.current.files[0]?.name || '未选择文件'
});
}
function handleFileChange() {
const file = fileRef.current.files[0];
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => setFilePreview(e.target.result);
reader.readAsDataURL(file);
} else {
setFilePreview(null);
}
}
function handleReset() {
nameRef.current.value = '';
emailRef.current.value = '';
fileRef.current.value = '';
setFilePreview(null);
setSubmittedData(null);
}
return (
<div>
<div className="panel">
<h3>非受控组件 - 使用ref获取表单值</h3>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>姓名</label>
<input type="text" ref={nameRef} defaultValue="默认姓名" placeholder="请输入姓名" />
</div>
<div className="form-group">
<label>邮箱</label>
<input type="email" ref={emailRef} placeholder="请输入邮箱" />
</div>
<div className="form-group">
<label>头像上传(文件输入必须用非受控)</label>
<input type="file" ref={fileRef} accept="image/*" onChange={handleFileChange} />
{filePreview && (
<div className="file-preview">
<img src={filePreview} alt="预览" />
</div>
)}
</div>
<button type="submit" className="btn-primary">提交</button>
<button type="button" className="btn-secondary" onClick={handleReset}>重置</button>
</form>
</div>
{submittedData && (
<div className="panel">
<h3>提交的数据</h3>
<pre>{JSON.stringify(submittedData, null, 2)}</pre>
</div>
)}
<div className="panel">
<h3>受控 vs 非受控对比</h3>
<div className="comparison">
<div className="comp-card controlled">
<h4 style={{ color: '#27ae60' }}>受控组件</h4>
<ul>
<li>值由React状态管理</li>
<li>每次输入都触发onChange</li>
<li>可以实时验证</li>
<li>可以条件性禁用输入</li>
<li>强制设置输入值</li>
</ul>
<pre>{`<input
value={name}
onChange={e =>
setName(e.target.value)
}
/>`}</pre>
</div>
<div className="comp-card uncontrolled">
<h4 style={{ color: '#f39c12' }}>非受控组件</h4>
<ul>
<li>值由DOM自身管理</li>
<li>使用ref获取值</li>
<li>使用defaultValue设置初始值</li>
<li>适合文件输入等场景</li>
<li>与非React代码集成方便</li>
</ul>
<pre>{`<input
ref={inputRef}
defaultValue="初始值"
/>
// 获取值
const val = inputRef.current.value;`}</pre>
</div>
</div>
</div>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('uncontrolled-demo'));
root.render(<UncontrolledForm />);
</script>
</body>
</html>表单验证与多表单处理
以下示例展示了如何结合自定义Hook实现表单验证、多步骤表单以及动态联系人列表等功能:
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>表单验证与多表单处理</title>
<style>
body { font-family: -apple-system, sans-serif; padding: 20px; max-width: 900px; margin: 0 auto; background: #f5f5f5; }
.panel { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); margin: 15px 0; }
.form-group { margin-bottom: 15px; }
.form-group label { display: block; margin-bottom: 5px; font-weight: bold; font-size: 14px; }
.form-group input, .form-group select { width: 100%; padding: 10px; border: 2px solid #e0e0e0; border-radius: 6px; font-size: 14px; box-sizing: border-box; transition: border-color 0.2s; }
.form-group input:focus, .form-group select:focus { outline: none; border-color: #667eea; }
.form-group input.valid { border-color: #27ae60; }
.form-group input.invalid { border-color: #e74c3c; }
.error-msg { color: #e74c3c; font-size: 12px; margin-top: 4px; }
.success-msg { color: #27ae60; font-size: 12px; margin-top: 4px; }
.password-strength { height: 4px; border-radius: 2px; margin-top: 8px; transition: all 0.3s; }
.strength-weak { background: #e74c3c; width: 33%; }
.strength-medium { background: #f39c12; width: 66%; }
.strength-strong { background: #27ae60; width: 100%; }
button { padding: 10px 24px; border: none; border-radius: 6px; cursor: pointer; font-size: 15px; margin: 5px; }
.btn-primary { background: #667eea; color: white; }
.btn-add { background: #27ae60; color: white; }
.btn-remove { background: #e74c3c; color: white; font-size: 13px; padding: 6px 12px; }
.step-indicator { display: flex; justify-content: center; margin-bottom: 20px; }
.step { padding: 8px 20px; border-radius: 20px; margin: 0 5px; font-size: 14px; }
.step-active { background: #667eea; color: white; }
.step-done { background: #27ae60; color: white; }
.step-pending { background: #e0e0e0; color: #999; }
.contact-row { display: flex; gap: 10px; align-items: flex-start; margin-bottom: 10px; padding: 10px; background: #f8f9fa; border-radius: 6px; }
.contact-row input { flex: 1; }
</style>
</head>
<body>
<h1>表单验证与多表单处理</h1>
<div id="validation-demo"></div>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script crossorigin src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
// 自定义Hook:表单验证
function useFormValidation(initialValues, validateFn) {
const [values, setValues] = React.useState(initialValues);
const [errors, setErrors] = React.useState({});
const [touched, setTouched] = React.useState({});
function handleChange(e) {
const { name, value } = e.target;
setValues(prev => ({ ...prev, [name]: value }));
if (touched[name]) {
const fieldErrors = validateFn({ ...values, [name]: value });
setErrors(prev => ({ ...prev, [name]: fieldErrors[name] || '' }));
}
}
function handleBlur(e) {
const { name } = e.target;
setTouched(prev => ({ ...prev, [name]: true }));
const fieldErrors = validateFn(values);
setErrors(prev => ({ ...prev, [name]: fieldErrors[name] || '' }));
}
function validateAll() {
const allErrors = validateFn(values);
setErrors(allErrors);
setTouched(Object.keys(initialValues).reduce((acc, key) => ({ ...acc, [key]: true }), {}));
return Object.keys(allErrors).every(key => !allErrors[key]);
}
return { values, errors, touched, handleChange, handleBlur, validateAll, setValues };
}
// 密码强度检测
function getPasswordStrength(password) {
if (!password) return 0;
let strength = 0;
if (password.length >= 6) strength++;
if (/[A-Z]/.test(password)) strength++;
if (/[0-9]/.test(password)) strength++;
if (/[^A-Za-z0-9]/.test(password)) strength++;
return strength;
}
function MultiStepForm() {
const [step, setStep] = React.useState(1);
const [contacts, setContacts] = React.useState([{ name: '', phone: '', relation: '' }]);
const validate = (vals) => {
const errs = {};
if (step === 1) {
if (!vals.fullName?.trim()) errs.fullName = '请输入姓名';
if (!vals.email?.trim()) errs.email = '请输入邮箱';
else if (!/\S+@\S+\.\S+/.test(vals.email)) errs.email = '邮箱格式不正确';
if (!vals.password) errs.password = '请输入密码';
else if (vals.password.length < 6) errs.password = '密码至少6位';
}
if (step === 2) {
if (!vals.city?.trim()) errs.city = '请输入城市';
if (!vals.address?.trim()) errs.address = '请输入地址';
}
return errs;
};
const { values, errors, touched, handleChange, handleBlur, validateAll } = useFormValidation(
{ fullName: '', email: '', password: '', city: '', address: '' },
validate
);
function nextStep() {
if (validateAll()) setStep(s => s + 1);
}
function prevStep() {
setStep(s => s - 1);
}
function addContact() {
setContacts([...contacts, { name: '', phone: '', relation: '' }]);
}
function removeContact(index) {
setContacts(contacts.filter((_, i) => i !== index));
}
function updateContact(index, field, value) {
const updated = [...contacts];
updated[index][field] = value;
setContacts(updated);
}
function handleSubmit(e) {
e.preventDefault();
if (validateAll()) {
alert('提交成功!\n' + JSON.stringify({ ...values, contacts }, null, 2));
}
}
const passwordStrength = getPasswordStrength(values.password);
const strengthLabel = ['', '弱', '中', '强', '很强'][passwordStrength];
const strengthClass = ['', 'strength-weak', 'strength-medium', 'strength-strong', 'strength-strong'][passwordStrength];
return (
<div className="panel">
<h3>多步骤表单</h3>
<div className="step-indicator">
<div className={`step ${step >= 1 ? (step > 1 ? 'step-done' : 'step-active') : 'step-pending'}`}>
{step > 1 ? '✓' : '1'} 基本信息
</div>
<div className={`step ${step >= 2 ? (step > 2 ? 'step-done' : 'step-active') : 'step-pending'}`}>
{step > 2 ? '✓' : '2'} 地址信息
</div>
<div className={`step ${step >= 3 ? 'step-active' : 'step-pending'}`}>
3 紧急联系人
</div>
</div>
<form onSubmit={handleSubmit}>
{step === 1 && (
<div>
<div className="form-group">
<label>姓名</label>
<input name="fullName" value={values.fullName} onChange={handleChange} onBlur={handleBlur}
className={touched.fullName ? (errors.fullName ? 'invalid' : 'valid') : ''} placeholder="请输入姓名" />
{touched.fullName && errors.fullName && <div className="error-msg">{errors.fullName}</div>}
</div>
<div className="form-group">
<label>邮箱</label>
<input name="email" type="email" value={values.email} onChange={handleChange} onBlur={handleBlur}
className={touched.email ? (errors.email ? 'invalid' : 'valid') : ''} placeholder="请输入邮箱" />
{touched.email && errors.email && <div className="error-msg">{errors.email}</div>}
{touched.email && !errors.email && values.email && <div className="success-msg">邮箱格式正确</div>}
</div>
<div className="form-group">
<label>密码</label>
<input name="password" type="password" value={values.password} onChange={handleChange} onBlur={handleBlur}
className={touched.password ? (errors.password ? 'invalid' : 'valid') : ''} placeholder="请输入密码" />
{values.password && <div className={`password-strength ${strengthClass}`}></div>}
{values.password && <div style={{ fontSize: '12px', color: '#666', marginTop: '4px' }}>密码强度: {strengthLabel}</div>}
{touched.password && errors.password && <div className="error-msg">{errors.password}</div>}
</div>
<button type="button" className="btn-primary" onClick={nextStep}>下一步</button>
</div>
)}
{step === 2 && (
<div>
<div className="form-group">
<label>城市</label>
<input name="city" value={values.city} onChange={handleChange} onBlur={handleBlur}
className={touched.city ? (errors.city ? 'invalid' : 'valid') : ''} placeholder="请输入城市" />
{touched.city && errors.city && <div className="error-msg">{errors.city}</div>}
</div>
<div className="form-group">
<label>详细地址</label>
<input name="address" value={values.address} onChange={handleChange} onBlur={handleBlur}
className={touched.address ? (errors.address ? 'invalid' : 'valid') : ''} placeholder="请输入详细地址" />
{touched.address && errors.address && <div className="error-msg">{errors.address}</div>}
</div>
<button type="button" className="btn-primary" onClick={prevStep}>上一步</button>
<button type="button" className="btn-primary" onClick={nextStep}>下一步</button>
</div>
)}
{step === 3 && (
<div>
<h4>紧急联系人</h4>
{contacts.map((contact, index) => (
<div key={index} className="contact-row">
<input placeholder="姓名" value={contact.name} onChange={e => updateContact(index, 'name', e.target.value)} />
<input placeholder="电话" value={contact.phone} onChange={e => updateContact(index, 'phone', e.target.value)} />
<select value={contact.relation} onChange={e => updateContact(index, 'relation', e.target.value)}>
<option value="">关系</option>
<option value="parent">父母</option>
<option value="spouse">配偶</option>
<option value="friend">朋友</option>
</select>
{contacts.length > 1 && (
<button type="button" className="btn-remove" onClick={() => removeContact(index)}>删除</button>
)}
</div>
))}
<button type="button" className="btn-add" onClick={addContact}>添加联系人</button>
<div style={{ marginTop: '15px' }}>
<button type="button" className="btn-primary" onClick={prevStep}>上一步</button>
<button type="submit" className="btn-primary">提交</button>
</div>
</div>
)}
</form>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('validation-demo'));
root.render(<MultiStepForm />);
</script>
</body>
</html>表单库简介
原生React表单处理需要大量样板代码:状态管理、验证逻辑、错误处理、提交处理等。表单库封装了这些通用逻辑,让开发者专注于业务。以下是React Hook Form和Formik两大主流表单库的使用示例:
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>React表单库</title>
<style>
body { font-family: -apple-system, sans-serif; padding: 20px; max-width: 900px; margin: 0 auto; background: #f5f5f5; }
.panel { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); margin: 15px 0; }
pre { background: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 8px; overflow-x: auto; font-size: 13px; }
.lib-card { border: 1px solid #e0e0e0; border-radius: 12px; padding: 20px; margin: 15px 0; }
.lib-card h3 { margin-top: 0; }
.lib-tag { display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 12px; font-weight: bold; margin-right: 5px; }
.tag-formik { background: #61dafb; color: #000; }
.tag-rhf { background: #ec5990; color: white; }
.tag-native { background: #27ae60; color: white; }
table { width: 100%; border-collapse: collapse; margin: 10px 0; }
th, td { border: 1px solid #ddd; padding: 10px; text-align: left; }
th { background: #667eea; color: white; }
.feature-list { columns: 2; }
.feature-list li { margin: 5px 0; }
</style>
</head>
<body>
<h1>React表单库简介</h1>
<div class="panel">
<h2>为什么需要表单库?</h2>
<p>原生React表单处理需要大量样板代码:状态管理、验证逻辑、错误处理、提交处理等。表单库封装了这些通用逻辑,让开发者专注于业务。</p>
</div>
<div class="lib-card">
<h3><span class="lib-tag tag-rhf">推荐</span> React Hook Form</h3>
<p>基于Hook的表单库,性能优秀,体积小。</p>
<pre>{`import { useForm } from 'react-hook-form';
function LoginForm() {
const {
register, // 注册表单字段
handleSubmit, // 处理表单提交
formState: { errors, isSubmitting }, // 表单状态
watch, // 监听字段值变化
reset, // 重置表单
setValue, // 设置字段值
getValues, // 获取字段值
} = useForm({
defaultValues: {
email: '',
password: '',
}
});
const onSubmit = (data) => {
console.log(data); // { email: '...', password: '...' }
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register('email', {
required: '邮箱不能为空',
pattern: {
value: /\\S+@\\S+\\.\\S+/,
message: '邮箱格式不正确'
}
})}
/>
{errors.email && <span>{errors.email.message}</span>}
<input
type="password"
{...register('password', {
required: '密码不能为空',
minLength: { value: 6, message: '密码至少6位' }
})}
/>
{errors.password && <span>{errors.password.message}</span>}
<button type="submit" disabled={isSubmitting}>登录</button>
</form>
);
}`}</pre>
<h4>核心优势</h4>
<ul class="feature-list">
<li>非受控为主,减少不必要的重渲染</li>
<li>体积小(~8KB gzipped)</li>
<li>内置验证规则</li>
<li>支持Yup/Zod等schema验证</li>
<li>与UI库集成方便</li>
<li>TypeScript友好</li>
</ul>
</div>
<div class="lib-card">
<h3><span class="lib-tag tag-formik">经典</span> Formik</h3>
<p>最流行的React表单库之一,基于受控组件。</p>
<pre>{`import { useFormik } from 'formik';
function LoginForm() {
const formik = useFormik({
initialValues: { email: '', password: '' },
validate: (values) => {
const errors = {};
if (!values.email) errors.email = '必填';
else if (!/\\S+@\\S+\\.\\S+/.test(values.email))
errors.email = '邮箱格式不正确';
return errors;
},
onSubmit: (values) => {
console.log(values);
},
});
return (
<form onSubmit={formik.handleSubmit}>
<input
name="email"
value={formik.values.email}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
/>
{formik.touched.email && formik.errors.email}
<button type="submit">提交</button>
</form>
);
}`}</pre>
</div>
</body>
</html>浏览器兼容性
以下是React表单相关特性在各主流浏览器中的兼容性情况:
注意事项与最佳实践
-
优先使用受控组件:受控组件让React完全控制表单数据,便于验证和条件渲染
-
文件输入必须用非受控:
<input type="file">的value是只读的,只能用ref获取文件 -
使用defaultValue设置初始值:非受控组件使用defaultValue而非value
-
表单验证时机:在onBlur时验证(用户离开输入框),而非每次onChange都验证
-
提取自定义Hook:将表单逻辑提取为useForm等自定义Hook,提高复用性
-
大型表单使用表单库:字段超过5个的表单建议使用React Hook Form
-
防止重复提交:提交时禁用按钮或使用isSubmitting状态
代码规范示例
以下是React表单开发中的推荐与不推荐写法对比:
代码示例
// 不推荐:每个字段单独管理状态
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
// 推荐:使用对象统一管理
const [formData, setFormData] = useState({
name: '', email: '', phone: ''
});
function handleChange(e) {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
}
// 不推荐:提交时才验证
function handleSubmit(e) {
e.preventDefault();
if (!name) alert('姓名不能为空');
if (!email) alert('邮箱不能为空');
}
// 推荐:实时验证 + 提交时全量验证
function handleBlur(e) {
validateField(e.target.name);
}
function handleSubmit(e) {
e.preventDefault();
if (validateAll()) { /* 提交 */ }
}
// 不推荐:内联复杂验证逻辑
<input onChange={(e) => {
const val = e.target.value;
if (val.length < 6) setError('太短');
else if (!/[A-Z]/.test(val)) setError('需要大写');
else if (!/[0-9]/.test(val)) setError('需要数字');
else setError('');
}} />
// 推荐:提取验证函数
function validatePassword(value) {
if (value.length < 6) return '太短';
if (!/[A-Z]/.test(value)) return '需要大写字母';
if (!/[0-9]/.test(value)) return '需要数字';
return '';
}常见问题与解决方案
常见问题
受控组件输入框无法输入怎么办?
常见原因是设置了value但没有onChange处理函数,或者onChange中没有正确更新状态。确保value={state}和onChange={e => setState(e.target.value)}配对使用。
textarea在React中的写法?
React中textarea使用value属性而非children:<textarea value={text} onChange={handleChange} />,而非HTML中的<textarea>文本</textarea>。
select的默认选中如何设置?
在受控组件中,通过value属性设置选中项:<select value={selectedValue} onChange={handleChange}>。非受控组件使用defaultValue。
如何处理大量表单字段的性能问题?
使用React Hook Form(非受控方式,减少重渲染),或将大表单拆分为多个子组件,每个子组件管理自己的状态。
表单数据如何提交到服务器?
使用fetch或axios发送数据。注意preventDefault阻止默认提交行为,然后手动发送请求。也可以使用FormData对象处理文件上传。
总结
本教程详细介绍了React表单与HTML的集成。受控组件是React推荐的表单处理方式,通过状态完全控制表单值,便于实时验证和条件渲染。非受控组件通过ref直接访问DOM,适用于文件输入等场景。表单验证应结合onBlur实时验证和提交时全量验证,多步骤表单需要合理管理步骤状态。
对于复杂表单,推荐使用React Hook Form等表单库来简化开发。理解受控与非受控的选择、验证策略和性能优化是构建高质量React表单的关键。下一教程将介绍React样式与HTML的集成。
提示:在实际项目中,建议根据表单复杂度选择合适的方案——简单表单用原生React受控组件,中大型表单优先使用React Hook Form,维护旧项目可考虑Formik。
本文涉及AI创作
内容由AI创作,请仔细甄别