Appearance
非受控组件详解
学习目标
通过本章学习,你将全面掌握:
- 非受控组件的概念和原理
- useRef获取DOM值
- defaultValue的使用
- 受控vs非受控的深入对比
- 文件上传处理
- 第三方库集成
- 混合使用策略
- React 19中的最佳实践
第一部分:非受控组件基础
1.1 什么是非受控组件
非受控组件是指表单元素的值由DOM自己管理,React通过ref在需要时获取值。
jsx
import { useRef } from 'react';
// 非受控组件
function UncontrolledInput() {
const inputRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
console.log('值:', inputRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<input ref={inputRef} defaultValue="初始值" />
<button type="submit">提交</button>
</form>
);
}
// 对比:受控组件
function ControlledInput() {
const [value, setValue] = useState('初始值');
const handleSubmit = (e) => {
e.preventDefault();
console.log('值:', value);
};
return (
<form onSubmit={handleSubmit}>
<input
value={value}
onChange={e => setValue(e.target.value)}
/>
<button type="submit">提交</button>
</form>
);
}1.2 非受控组件的特点
jsx
function UncontrolledCharacteristics() {
const nameRef = useRef(null);
const emailRef = useRef(null);
const passwordRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
// 特点1:只在需要时读取值
const formData = {
name: nameRef.current.value,
email: emailRef.current.value,
password: passwordRef.current.value
};
console.log('表单数据:', formData);
// 特点2:可以直接操作DOM
nameRef.current.focus();
nameRef.current.select();
// 特点3:可以重置表单
nameRef.current.value = '';
emailRef.current.value = '';
passwordRef.current.value = '';
};
return (
<form onSubmit={handleSubmit}>
<input ref={nameRef} defaultValue="" placeholder="姓名" />
<input ref={emailRef} defaultValue="" placeholder="邮箱" />
<input ref={passwordRef} type="password" defaultValue="" placeholder="密码" />
<button type="submit">提交</button>
</form>
);
}1.3 defaultValue vs value
jsx
function DefaultValueVsValue() {
// defaultValue(非受控)
function DefaultValueExample() {
const inputRef = useRef(null);
return (
<div>
<input ref={inputRef} defaultValue="初始值" />
{/* 用户可以自由修改,React不控制 */}
<button onClick={() => {
console.log('当前值:', inputRef.current.value);
}}>
获取值
</button>
<button onClick={() => {
inputRef.current.value = '新值';
}}>
设置值
</button>
</div>
);
}
// value(受控)
function ValueExample() {
const [value, setValue] = useState('初始值');
return (
<div>
<input
value={value}
onChange={e => setValue(e.target.value)}
/>
{/* React完全控制值的变化 */}
<button onClick={() => console.log('当前值:', value)}>
获取值
</button>
<button onClick={() => setValue('新值')}>
设置值
</button>
</div>
);
}
return (
<div>
<h3>非受控(defaultValue)</h3>
<DefaultValueExample />
<h3>受控(value)</h3>
<ValueExample />
</div>
);
}第二部分:各种非受控元素
2.1 input元素
jsx
function UncontrolledInputs() {
const textRef = useRef(null);
const numberRef = useRef(null);
const dateRef = useRef(null);
const emailRef = useRef(null);
const passwordRef = useRef(null);
const urlRef = useRef(null);
const telRef = useRef(null);
const searchRef = useRef(null);
const colorRef = useRef(null);
const rangeRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
const formData = {
text: textRef.current.value,
number: numberRef.current.value,
date: dateRef.current.value,
email: emailRef.current.value,
password: passwordRef.current.value,
url: urlRef.current.value,
tel: telRef.current.value,
search: searchRef.current.value,
color: colorRef.current.value,
range: rangeRef.current.value
};
console.log('表单数据:', formData);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>文本:</label>
<input ref={textRef} type="text" defaultValue="" />
</div>
<div>
<label>数字:</label>
<input ref={numberRef} type="number" defaultValue={0} />
</div>
<div>
<label>日期:</label>
<input ref={dateRef} type="date" />
</div>
<div>
<label>邮箱:</label>
<input ref={emailRef} type="email" defaultValue="" />
</div>
<div>
<label>密码:</label>
<input ref={passwordRef} type="password" defaultValue="" />
</div>
<div>
<label>网址:</label>
<input ref={urlRef} type="url" defaultValue="" />
</div>
<div>
<label>电话:</label>
<input ref={telRef} type="tel" defaultValue="" />
</div>
<div>
<label>搜索:</label>
<input ref={searchRef} type="search" defaultValue="" />
</div>
<div>
<label>颜色:</label>
<input ref={colorRef} type="color" defaultValue="#000000" />
</div>
<div>
<label>范围:</label>
<input ref={rangeRef} type="range" min="0" max="100" defaultValue={50} />
<span id="rangeValue">50</span>
</div>
<button type="submit">提交</button>
</form>
);
}2.2 textarea元素
jsx
function UncontrolledTextarea() {
const textareaRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
console.log('内容:', textareaRef.current.value);
};
const handleClear = () => {
textareaRef.current.value = '';
textareaRef.current.focus();
};
const handleInsertText = () => {
const textarea = textareaRef.current;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const text = textarea.value;
const before = text.substring(0, start);
const after = text.substring(end);
const insert = '[插入的文本]';
textarea.value = before + insert + after;
textarea.selectionStart = textarea.selectionEnd = start + insert.length;
textarea.focus();
};
return (
<div>
<textarea
ref={textareaRef}
defaultValue="默认内容"
rows={10}
cols={50}
placeholder="请输入内容..."
/>
<div>
<button onClick={handleClear}>清空</button>
<button onClick={handleInsertText}>插入文本</button>
<button onClick={handleSubmit}>提交</button>
</div>
</div>
);
}2.3 select下拉框
jsx
function UncontrolledSelect() {
const selectRef = useRef(null);
const multiSelectRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
// 单选
const singleValue = selectRef.current.value;
// 多选
const multiOptions = Array.from(multiSelectRef.current.selectedOptions);
const multiValues = multiOptions.map(opt => opt.value);
console.log({
single: singleValue,
multiple: multiValues
});
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>单选下拉框:</label>
<select ref={selectRef} defaultValue="banana">
<option value="">请选择</option>
<option value="apple">苹果</option>
<option value="banana">香蕉</option>
<option value="orange">橙子</option>
</select>
</div>
<div>
<label>多选下拉框:</label>
<select
ref={multiSelectRef}
multiple
defaultValue={['apple', 'banana']}
>
<option value="apple">苹果</option>
<option value="banana">香蕉</option>
<option value="orange">橙子</option>
<option value="grape">葡萄</option>
</select>
</div>
<button type="submit">提交</button>
</form>
);
}2.4 checkbox和radio
jsx
function UncontrolledCheckboxRadio() {
const checkboxRef = useRef(null);
const radioRefs = {
option1: useRef(null),
option2: useRef(null),
option3: useRef(null)
};
// 多个checkbox的refs
const hobbiesRefs = {
reading: useRef(null),
sports: useRef(null),
music: useRef(null),
travel: useRef(null)
};
const handleSubmit = (e) => {
e.preventDefault();
// 单个checkbox
const agreeToTerms = checkboxRef.current.checked;
// 单选框
let selectedOption = null;
for (const [key, ref] of Object.entries(radioRefs)) {
if (ref.current.checked) {
selectedOption = key;
break;
}
}
// 多个checkbox
const selectedHobbies = [];
for (const [hobby, ref] of Object.entries(hobbiesRefs)) {
if (ref.current.checked) {
selectedHobbies.push(hobby);
}
}
console.log({
agreeToTerms,
selectedOption,
selectedHobbies
});
};
return (
<form onSubmit={handleSubmit}>
{/* 单个checkbox */}
<div>
<label>
<input
ref={checkboxRef}
type="checkbox"
defaultChecked={false}
/>
同意条款
</label>
</div>
{/* 单选框组 */}
<div>
<h4>选择一个选项:</h4>
{Object.keys(radioRefs).map(key => (
<label key={key}>
<input
ref={radioRefs[key]}
type="radio"
name="choice"
value={key}
defaultChecked={key === 'option1'}
/>
{key}
</label>
))}
</div>
{/* 多个checkbox */}
<div>
<h4>选择爱好:</h4>
{Object.entries(hobbiesRefs).map(([hobby, ref]) => (
<label key={hobby}>
<input
ref={ref}
type="checkbox"
defaultChecked={false}
/>
{hobby}
</label>
))}
</div>
<button type="submit">提交</button>
</form>
);
}2.5 文件上传
jsx
function UncontrolledFileUpload() {
const fileRef = useRef(null);
const multipleFileRef = useRef(null);
const [uploadedFiles, setUploadedFiles] = useState([]);
const [uploadProgress, setUploadProgress] = useState(0);
// 单文件上传
const handleSingleUpload = (e) => {
e.preventDefault();
const file = fileRef.current.files[0];
if (!file) {
alert('请选择文件');
return;
}
console.log('文件信息:', {
name: file.name,
size: file.size,
type: file.type,
lastModified: new Date(file.lastModified)
});
uploadFile(file);
};
// 多文件上传
const handleMultipleUpload = (e) => {
e.preventDefault();
const files = Array.from(multipleFileRef.current.files);
if (files.length === 0) {
alert('请选择文件');
return;
}
console.log(`选择了 ${files.length} 个文件`);
files.forEach(file => uploadFile(file));
};
// 文件上传函数
const uploadFile = (file) => {
const formData = new FormData();
formData.append('file', file);
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
const percent = (e.loaded / e.total) * 100;
setUploadProgress(percent);
}
};
xhr.onload = () => {
if (xhr.status === 200) {
setUploadedFiles(prev => [...prev, file.name]);
setUploadProgress(0);
alert('上传成功!');
}
};
xhr.onerror = () => {
alert('上传失败');
setUploadProgress(0);
};
xhr.open('POST', '/api/upload');
xhr.send(formData);
};
return (
<div className="file-upload">
<h3>单文件上传</h3>
<form onSubmit={handleSingleUpload}>
<input
ref={fileRef}
type="file"
accept="image/*"
/>
<button type="submit">上传</button>
</form>
<h3>多文件上传</h3>
<form onSubmit={handleMultipleUpload}>
<input
ref={multipleFileRef}
type="file"
multiple
accept="image/*,application/pdf"
/>
<button type="submit">上传全部</button>
</form>
{uploadProgress > 0 && (
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${uploadProgress}%` }}
/>
<span>{uploadProgress.toFixed(0)}%</span>
</div>
)}
{uploadedFiles.length > 0 && (
<div className="uploaded-files">
<h4>已上传的文件:</h4>
<ul>
{uploadedFiles.map((filename, i) => (
<li key={i}>{filename}</li>
))}
</ul>
</div>
)}
</div>
);
}第三部分:受控vs非受控对比
3.1 何时使用受控组件
jsx
// 场景1:需要实时验证
function RealTimeValidation() {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
const handleChange = (e) => {
const value = e.target.value;
setEmail(value);
// 实时验证
if (!value) {
setError('');
} else if (!/\S+@\S+\.\S+/.test(value)) {
setError('邮箱格式不正确');
} else {
setError('');
}
};
return (
<div>
<input
value={email}
onChange={handleChange}
placeholder="输入邮箱"
/>
{error && <span className="error">{error}</span>}
{!error && email && <span className="success">格式正确</span>}
</div>
);
}
// 场景2:需要格式化输入
function FormattedInput() {
const [phone, setPhone] = useState('');
const handleChange = (e) => {
let value = e.target.value.replace(/\D/g, ''); // 只保留数字
// 格式化为 xxx-xxxx-xxxx
if (value.length > 3 && value.length <= 7) {
value = `${value.slice(0, 3)}-${value.slice(3)}`;
} else if (value.length > 7) {
value = `${value.slice(0, 3)}-${value.slice(3, 7)}-${value.slice(7, 11)}`;
}
setPhone(value);
};
return (
<div>
<input
value={phone}
onChange={handleChange}
placeholder="输入手机号"
/>
<p>格式化后: {phone}</p>
</div>
);
}
// 场景3:需要禁用某些输入
function RestrictedInput() {
const [value, setValue] = useState('');
const handleChange = (e) => {
const newValue = e.target.value;
// 限制长度
if (newValue.length <= 10) {
setValue(newValue);
}
};
const handleKeyPress = (e) => {
// 禁止输入数字
if (/[0-9]/.test(e.key)) {
e.preventDefault();
}
};
return (
<div>
<input
value={value}
onChange={handleChange}
onKeyPress={handleKeyPress}
placeholder="最多10个字符,不能输入数字"
/>
<p>{value.length}/10</p>
</div>
);
}
// 场景4:需要联动多个输入
function LinkedInputs() {
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
const handleStartChange = (e) => {
const value = e.target.value;
setStartDate(value);
// 联动:如果结束日期早于开始日期,清空结束日期
if (endDate && value > endDate) {
setEndDate('');
}
};
return (
<div>
<input
type="date"
value={startDate}
onChange={handleStartChange}
/>
<span>至</span>
<input
type="date"
value={endDate}
onChange={e => setEndDate(e.target.value)}
min={startDate} // 最小日期为开始日期
/>
</div>
);
}3.2 何时使用非受控组件
jsx
// 场景1:简单表单
function SimpleForm() {
const nameRef = useRef(null);
const emailRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
const data = {
name: nameRef.current.value,
email: emailRef.current.value
};
console.log('提交:', data);
// 重置表单
e.target.reset();
};
return (
<form onSubmit={handleSubmit}>
<input ref={nameRef} name="name" placeholder="姓名" />
<input ref={emailRef} name="email" placeholder="邮箱" />
<button type="submit">提交</button>
</form>
);
}
// 场景2:文件上传
function FileUploadUncontrolled() {
const fileRef = useRef(null);
const [preview, setPreview] = useState(null);
const handleFileChange = () => {
const file = fileRef.current.files[0];
if (file && file.type.startsWith('image/')) {
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) return;
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
if (response.ok) {
alert('上传成功');
setPreview(null);
fileRef.current.value = '';
}
} catch (error) {
alert('上传失败');
}
};
return (
<div>
<input
ref={fileRef}
type="file"
accept="image/*"
onChange={handleFileChange}
/>
{preview && (
<div className="preview">
<img src={preview} alt="预览" style={{ maxWidth: '300px' }} />
</div>
)}
<button onClick={handleUpload}>上传</button>
</div>
);
}
// 场景3:集成第三方库
function ThirdPartyLibraryIntegration() {
const editorRef = useRef(null);
const chartRef = useRef(null);
const mapRef = useRef(null);
useEffect(() => {
// 初始化富文本编辑器
const editor = new RichTextEditor(editorRef.current, {
placeholder: '请输入内容...',
theme: 'snow'
});
// 初始化图表
const chart = new Chart(chartRef.current, {
type: 'bar',
data: { /* ... */ }
});
// 初始化地图
const map = new LeafletMap(mapRef.current, {
center: [51.505, -0.09],
zoom: 13
});
return () => {
// 清理
editor.destroy();
chart.destroy();
map.remove();
};
}, []);
return (
<div>
<div ref={editorRef} />
<canvas ref={chartRef} />
<div ref={mapRef} style={{ height: '400px' }} />
</div>
);
}
// 场景4:需要直接操作DOM
function DirectDOMManipulation() {
const videoRef = useRef(null);
const canvasRef = useRef(null);
const handlePlay = () => {
videoRef.current.play();
};
const handlePause = () => {
videoRef.current.pause();
};
const handleCapture = () => {
const video = videoRef.current;
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
ctx.drawImage(video, 0, 0);
};
return (
<div>
<video
ref={videoRef}
src="/video.mp4"
width="640"
height="360"
/>
<div>
<button onClick={handlePlay}>播放</button>
<button onClick={handlePause}>暂停</button>
<button onClick={handleCapture}>截图</button>
</div>
<canvas ref={canvasRef} style={{ display: 'block', marginTop: '20px' }} />
</div>
);
}第四部分:高级用法
4.1 获取焦点
jsx
function FocusManagement() {
const inputRef = useRef(null);
const textareaRef = useRef(null);
const selectRef = useRef(null);
useEffect(() => {
// 组件挂载时自动聚焦
inputRef.current.focus();
}, []);
const focusInput = () => {
inputRef.current.focus();
};
const focusTextarea = () => {
textareaRef.current.focus();
};
const focusSelect = () => {
selectRef.current.focus();
};
const selectAll = () => {
inputRef.current.select();
};
return (
<div>
<input ref={inputRef} defaultValue="输入框" />
<textarea ref={textareaRef} defaultValue="文本域" />
<select ref={selectRef}>
<option>选项1</option>
<option>选项2</option>
</select>
<div>
<button onClick={focusInput}>聚焦输入框</button>
<button onClick={focusTextarea}>聚焦文本域</button>
<button onClick={focusSelect}>聚焦下拉框</button>
<button onClick={selectAll}>全选输入框</button>
</div>
</div>
);
}4.2 表单重置
jsx
function FormReset() {
const formRef = useRef(null);
const nameRef = useRef(null);
const emailRef = useRef(null);
const handleReset = () => {
// 方式1:使用表单的reset方法
formRef.current.reset();
};
const handleManualReset = () => {
// 方式2:手动重置每个字段
nameRef.current.value = '';
emailRef.current.value = '';
};
const handleResetToDefault = () => {
// 方式3:重置为默认值
nameRef.current.value = nameRef.current.defaultValue;
emailRef.current.value = emailRef.current.defaultValue;
};
return (
<form ref={formRef}>
<input
ref={nameRef}
name="name"
defaultValue="张三"
placeholder="姓名"
/>
<input
ref={emailRef}
name="email"
defaultValue="zhangsan@example.com"
placeholder="邮箱"
/>
<div>
<button type="button" onClick={handleReset}>
表单重置
</button>
<button type="button" onClick={handleManualReset}>
手动重置
</button>
<button type="button" onClick={handleResetToDefault}>
恢复默认值
</button>
</div>
</form>
);
}4.3 表单验证
jsx
function UncontrolledValidation() {
const formRef = useRef(null);
const [errors, setErrors] = useState({});
const validateForm = () => {
const formData = new FormData(formRef.current);
const newErrors = {};
const name = formData.get('name');
if (!name) {
newErrors.name = '姓名不能为空';
} else if (name.length < 2) {
newErrors.name = '姓名至少2个字符';
}
const email = formData.get('email');
if (!email) {
newErrors.email = '邮箱不能为空';
} else if (!/\S+@\S+\.\S+/.test(email)) {
newErrors.email = '邮箱格式错误';
}
const age = formData.get('age');
if (!age) {
newErrors.age = '年龄不能为空';
} else if (age < 0 || age > 120) {
newErrors.age = '年龄必须在0-120之间';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e) => {
e.preventDefault();
if (validateForm()) {
const formData = new FormData(formRef.current);
const data = Object.fromEntries(formData);
console.log('表单数据:', data);
alert('提交成功!');
}
};
return (
<form ref={formRef} onSubmit={handleSubmit}>
<div>
<label>姓名:</label>
<input name="name" defaultValue="" />
{errors.name && <span className="error">{errors.name}</span>}
</div>
<div>
<label>邮箱:</label>
<input name="email" type="email" defaultValue="" />
{errors.email && <span className="error">{errors.email}</span>}
</div>
<div>
<label>年龄:</label>
<input name="age" type="number" defaultValue="" />
{errors.age && <span className="error">{errors.age}</span>}
</div>
<button type="submit">提交</button>
</form>
);
}第五部分:混合使用策略
5.1 部分字段受控,部分非受控
jsx
function HybridForm() {
// 受控:需要实时反馈的字段
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
// 非受控:不需要实时反馈的字段
const phoneRef = useRef(null);
const addressRef = useRef(null);
const remarksRef = useRef(null);
// 实时验证username
const isUsernameValid = username.length >= 3;
// 实时验证email
const isEmailValid = /\S+@\S+\.\S+/.test(email);
const handleSubmit = (e) => {
e.preventDefault();
if (!isUsernameValid || !isEmailValid) {
alert('请检查表单');
return;
}
const formData = {
// 受控组件的值
username,
email,
// 非受控组件的值
phone: phoneRef.current.value,
address: addressRef.current.value,
remarks: remarksRef.current.value
};
console.log('提交:', formData);
};
return (
<form onSubmit={handleSubmit}>
{/* 受控字段:需要实时验证 */}
<div>
<label>用户名(受控):</label>
<input
value={username}
onChange={e => setUsername(e.target.value)}
placeholder="至少3个字符"
/>
{username && (
<span className={isUsernameValid ? 'valid' : 'invalid'}>
{isUsernameValid ? '✓' : '用户名太短'}
</span>
)}
</div>
<div>
<label>邮箱(受控):</label>
<input
value={email}
onChange={e => setEmail(e.target.value)}
placeholder="输入邮箱"
/>
{email && (
<span className={isEmailValid ? 'valid' : 'invalid'}>
{isEmailValid ? '✓' : '邮箱格式错误'}
</span>
)}
</div>
{/* 非受控字段:不需要实时验证 */}
<div>
<label>电话(非受控):</label>
<input ref={phoneRef} defaultValue="" placeholder="电话号码" />
</div>
<div>
<label>地址(非受控):</label>
<textarea ref={addressRef} defaultValue="" placeholder="详细地址" />
</div>
<div>
<label>备注(非受控):</label>
<textarea ref={remarksRef} defaultValue="" placeholder="其他信息" />
</div>
<button type="submit">提交</button>
</form>
);
}5.2 条件切换受控/非受控
jsx
function ConditionalControl() {
const [isControlled, setIsControlled] = useState(true);
const [controlledValue, setControlledValue] = useState('');
const uncontrolledRef = useRef(null);
const getValue = () => {
if (isControlled) {
return controlledValue;
} else {
return uncontrolledRef.current?.value || '';
}
};
const setValue = (value) => {
if (isControlled) {
setControlledValue(value);
} else if (uncontrolledRef.current) {
uncontrolledRef.current.value = value;
}
};
return (
<div>
<label>
<input
type="checkbox"
checked={isControlled}
onChange={e => setIsControlled(e.target.checked)}
/>
使用受控组件
</label>
{isControlled ? (
<input
value={controlledValue}
onChange={e => setControlledValue(e.target.value)}
placeholder="受控输入"
/>
) : (
<input
ref={uncontrolledRef}
defaultValue=""
placeholder="非受控输入"
/>
)}
<div>
<p>当前值: {getValue()}</p>
<button onClick={() => setValue('新值')}>设置为"新值"</button>
<button onClick={() => setValue('')}>清空</button>
</div>
</div>
);
}第六部分:实战案例
6.1 案例1:登录表单
jsx
function LoginForm() {
const usernameRef = useRef(null);
const passwordRef = useRef(null);
const rememberRef = useRef(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setLoading(true);
const credentials = {
username: usernameRef.current.value,
password: passwordRef.current.value,
remember: rememberRef.current.checked
};
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
if (response.ok) {
const data = await response.json();
console.log('登录成功:', data);
if (credentials.remember) {
localStorage.setItem('username', credentials.username);
}
} else {
setError('用户名或密码错误');
}
} catch (error) {
setError('登录失败,请重试');
} finally {
setLoading(false);
}
};
// 组件挂载时恢复记住的用户名
useEffect(() => {
const savedUsername = localStorage.getItem('username');
if (savedUsername) {
usernameRef.current.value = savedUsername;
rememberRef.current.checked = true;
}
}, []);
return (
<form onSubmit={handleSubmit} className="login-form">
<h2>登录</h2>
{error && <div className="error">{error}</div>}
<div>
<input
ref={usernameRef}
type="text"
placeholder="用户名"
required
/>
</div>
<div>
<input
ref={passwordRef}
type="password"
placeholder="密码"
required
/>
</div>
<div>
<label>
<input
ref={rememberRef}
type="checkbox"
defaultChecked={false}
/>
记住我
</label>
</div>
<button type="submit" disabled={loading}>
{loading ? '登录中...' : '登录'}
</button>
</form>
);
}6.2 案例2:图片上传预览
jsx
function ImageUploadPreview() {
const fileRef = useRef(null);
const [images, setImages] = useState([]);
const handleFileSelect = () => {
const files = Array.from(fileRef.current.files);
const newImages = files.map(file => ({
id: Date.now() + Math.random(),
file,
preview: URL.createObjectURL(file),
name: file.name,
size: file.size
}));
setImages(prev => [...prev, ...newImages]);
};
const removeImage = (id) => {
setImages(prev => {
const image = prev.find(img => img.id === id);
if (image) {
URL.revokeObjectURL(image.preview);
}
return prev.filter(img => img.id !== id);
});
};
const handleUploadAll = async () => {
for (const image of images) {
const formData = new FormData();
formData.append('file', image.file);
try {
await fetch('/api/upload', {
method: 'POST',
body: formData
});
console.log('上传成功:', image.name);
} catch (error) {
console.error('上传失败:', image.name);
}
}
alert('所有图片上传完成');
setImages([]);
fileRef.current.value = '';
};
// 清理预览URL
useEffect(() => {
return () => {
images.forEach(image => {
URL.revokeObjectURL(image.preview);
});
};
}, [images]);
return (
<div className="image-upload">
<input
ref={fileRef}
type="file"
accept="image/*"
multiple
onChange={handleFileSelect}
style={{ display: 'none' }}
/>
<button onClick={() => fileRef.current.click()}>
选择图片
</button>
{images.length > 0 && (
<>
<div className="preview-grid">
{images.map(image => (
<div key={image.id} className="preview-item">
<img src={image.preview} alt={image.name} />
<div className="image-info">
<p>{image.name}</p>
<p>{(image.size / 1024).toFixed(2)} KB</p>
</div>
<button
onClick={() => removeImage(image.id)}
className="remove-btn"
>
×
</button>
</div>
))}
</div>
<button onClick={handleUploadAll} className="upload-btn">
上传全部 ({images.length})
</button>
</>
)}
</div>
);
}6.3 案例3:富文本编辑器集成
jsx
// 集成Quill编辑器
import { useRef, useEffect } from 'react';
import Quill from 'quill';
import 'quill/dist/quill.snow.css';
function RichTextEditor({ onSave }) {
const editorRef = useRef(null);
const quillRef = useRef(null);
useEffect(() => {
// 初始化Quill
quillRef.current = new Quill(editorRef.current, {
theme: 'snow',
modules: {
toolbar: [
[{ header: [1, 2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
[{ list: 'ordered' }, { list: 'bullet' }],
[{ color: [] }, { background: [] }],
['link', 'image'],
['clean']
]
},
placeholder: '请输入内容...'
});
// 监听内容变化
quillRef.current.on('text-change', () => {
console.log('内容变化');
});
return () => {
quillRef.current = null;
};
}, []);
const handleSave = () => {
const html = quillRef.current.root.innerHTML;
const text = quillRef.current.getText();
const delta = quillRef.current.getContents();
onSave({
html,
text,
delta
});
};
const handleClear = () => {
quillRef.current.setContents([]);
};
const handleInsertImage = () => {
const range = quillRef.current.getSelection(true);
quillRef.current.insertEmbed(range.index, 'image', 'https://example.com/image.jpg');
};
return (
<div className="rich-text-editor">
<div ref={editorRef} style={{ height: '300px' }} />
<div className="editor-actions">
<button onClick={handleSave}>保存</button>
<button onClick={handleClear}>清空</button>
<button onClick={handleInsertImage}>插入图片</button>
</div>
</div>
);
}6.4 案例4:数据采集表单
jsx
function DataCollectionForm() {
const formRef = useRef(null);
const [submittedData, setSubmittedData] = useState([]);
const handleSubmit = (e) => {
e.preventDefault();
// 使用FormData API获取所有表单数据
const formData = new FormData(formRef.current);
const data = Object.fromEntries(formData);
// 处理checkbox group
const hobbies = formData.getAll('hobbies');
data.hobbies = hobbies;
// 添加时间戳
data.timestamp = new Date().toISOString();
setSubmittedData(prev => [...prev, data]);
// 重置表单
formRef.current.reset();
};
const exportData = () => {
const json = JSON.stringify(submittedData, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `data-${Date.now()}.json`;
link.click();
URL.revokeObjectURL(url);
};
return (
<div>
<form ref={formRef} onSubmit={handleSubmit}>
<h3>数据采集表单</h3>
<div>
<label>姓名:</label>
<input name="name" defaultValue="" required />
</div>
<div>
<label>年龄:</label>
<input name="age" type="number" defaultValue="" required />
</div>
<div>
<label>性别:</label>
<label><input type="radio" name="gender" value="male" /> 男</label>
<label><input type="radio" name="gender" value="female" /> 女</label>
</div>
<div>
<label>爱好:</label>
<label><input type="checkbox" name="hobbies" value="reading" /> 阅读</label>
<label><input type="checkbox" name="hobbies" value="sports" /> 运动</label>
<label><input type="checkbox" name="hobbies" value="music" /> 音乐</label>
</div>
<div>
<label>城市:</label>
<select name="city" defaultValue="">
<option value="">请选择</option>
<option value="beijing">北京</option>
<option value="shanghai">上海</option>
<option value="guangzhou">广州</option>
</select>
</div>
<div>
<label>备注:</label>
<textarea name="remarks" defaultValue="" rows={3} />
</div>
<button type="submit">提交</button>
</form>
{submittedData.length > 0 && (
<div>
<h3>已收集数据 ({submittedData.length})</h3>
<button onClick={exportData}>导出JSON</button>
<table>
<thead>
<tr>
<th>姓名</th>
<th>年龄</th>
<th>性别</th>
<th>爱好</th>
<th>城市</th>
<th>时间</th>
</tr>
</thead>
<tbody>
{submittedData.map((data, i) => (
<tr key={i}>
<td>{data.name}</td>
<td>{data.age}</td>
<td>{data.gender}</td>
<td>{data.hobbies.join(', ')}</td>
<td>{data.city}</td>
<td>{new Date(data.timestamp).toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
}第七部分:最佳实践
7.1 选择受控还是非受控
jsx
// 决策流程图
const DecisionTree = {
// 需要实时验证 → 使用受控
realtimeValidation: {
answer: '是',
use: '受控组件',
example: '<input value={email} onChange={validate} />'
},
// 需要格式化输入 → 使用受控
formatInput: {
answer: '是',
use: '受控组件',
example: '<input value={phone} onChange={formatPhone} />'
},
// 需要禁用某些输入 → 使用受控
restrictInput: {
answer: '是',
use: '受控组件',
example: '<input value={value} onChange={restrict} />'
},
// 简单表单,只在提交时需要值 → 使用非受控
simpleForm: {
answer: '否',
use: '非受控组件',
example: '<input ref={inputRef} defaultValue="" />'
},
// 文件上传 → 使用非受控
fileUpload: {
answer: '总是',
use: '非受控组件',
example: '<input ref={fileRef} type="file" />'
},
// 集成第三方库 → 使用非受控
thirdParty: {
answer: '总是',
use: '非受控组件',
example: '<div ref={editorRef} />'
}
};7.2 性能考虑
jsx
// 受控组件的性能问题
function ControlledPerformance() {
const [value, setValue] = useState('');
const renderCount = useRef(0);
useEffect(() => {
renderCount.current++;
});
// 每次输入都触发渲染
const handleChange = (e) => {
setValue(e.target.value); // 触发渲染
};
return (
<div>
<p>渲染次数: {renderCount.current}</p>
<input value={value} onChange={handleChange} />
</div>
);
}
// 非受控组件避免频繁渲染
function UncontrolledPerformance() {
const inputRef = useRef(null);
const renderCount = useRef(0);
useEffect(() => {
renderCount.current++;
});
// 输入不触发渲染
const getValue = () => {
console.log('值:', inputRef.current.value);
};
return (
<div>
<p>渲染次数: {renderCount.current}</p>
<input ref={inputRef} defaultValue="" />
<button onClick={getValue}>获取值</button>
</div>
);
}练习题
基础练习
- 实现一个非受控表单,包含多种输入类型
- 使用ref获取和设置input的值
- 实现表单的重置功能
- 对比受控和非受控组件的渲染次数
进阶练习
- 实现一个文件上传组件,支持多文件和预览
- 创建一个混合使用受控和非受控的复杂表单
- 集成一个第三方富文本编辑器
- 实现表单数据的导出功能
高级练习
- 实现一个性能优化的大型表单
- 创建一个表单构建器,支持动态添加字段
- 实现表单的自动保存功能(使用非受控组件)
通过本章学习,你已经掌握了非受控组件的完整知识。理解受控和非受控的区别,能让你选择最适合的方案。在实际开发中,根据具体需求灵活选择,有时混合使用能达到最佳效果!
继续学习,成为React表单处理专家!