表单与受控组件一、受控组件 vs 非受控组件1.1 概念对比类型数据来源更新方式适用场景受控组件React stateonChange 事件更新大多数场景非受控组件DOM 自身ref 获取值简单表单、文件上传1.2 受控组件示例function ControlledInput() { const [value, setValue] useState(); const handleChange (e) { setValue(e.target.value); }; return ( input typetext value{value} onChange{handleChange} placeholder输入内容... / ); }1.3 非受控组件示例function UncontrolledInput() { const inputRef useRef(null); const handleSubmit () { alert(输入的值: inputRef.current.value); }; return ( div input typetext ref{inputRef} defaultValue默认值 / button onClick{handleSubmit}获取值/button /div ); }二、常见表单元素2.1 文本输入框function TextInput() { const [text, setText] useState(); return ( div label 用户名 input typetext value{text} onChange{(e) setText(e.target.value)} placeholder请输入用户名 / /label p当前输入{text}/p /div ); }2.2 文本域 (Textarea)function TextareaExample() { const [content, setContent] useState(); return ( div label 个人简介 textarea value{content} onChange{(e) setContent(e.target.value)} rows{5} cols{40} placeholder介绍一下自己... / /label p字符数{content.length}/p /div ); }2.3 单选框 (Radio)function RadioGroup() { const [gender, setGender] useState(); return ( div label input typeradio valuemale checked{gender male} onChange{(e) setGender(e.target.value)} / 男 /label label input typeradio valuefemale checked{gender female} onChange{(e) setGender(e.target.value)} / 女 /label p选择的性别{gender}/p /div ); }2.4 复选框 (Checkbox)function CheckboxExample() { const [isAgreed, setIsAgreed] useState(false); const [interests, setInterests] useState([]); const handleInterestChange (interest) { setInterests(prev prev.includes(interest) ? prev.filter(i i ! interest) : [...prev, interest] ); }; return ( div label input typecheckbox checked{isAgreed} onChange{(e) setIsAgreed(e.target.checked)} / 同意条款 /label div 兴趣爱好 {[阅读, 音乐, 运动].map(interest ( label key{interest} input typecheckbox value{interest} checked{interests.includes(interest)} onChange{() handleInterestChange(interest)} / {interest} /label ))} /div /div ); }2.5 下拉选择框 (Select)function SelectExample() { const [city, setCity] useState(); const [cities, setCities] useState([]); const handleMultipleChange (e) { const options Array.from(e.target.selectedOptions); setCities(options.map(opt opt.value)); }; return ( div {/* 单选 */} select value{city} onChange{(e) setCity(e.target.value)} option value请选择城市/option option valuebeijing北京/option option valueshanghai上海/option option valueguangzhou广州/option /select {/* 多选 */} select multiple value{cities} onChange{handleMultipleChange} option valuebeijing北京/option option valueshanghai上海/option option valueguangzhou广州/option /select /div ); }三、复杂表单处理3.1 表单状态管理function RegistrationForm() { const [formData, setFormData] useState({ username: , email: , password: , confirmPassword: , age: , gender: , agree: false }); const handleChange (e) { const { name, value, type, checked } e.target; setFormData(prev ({ ...prev, [name]: type checkbox ? checked : value })); }; return ( form input nameusername value{formData.username} onChange{handleChange} placeholder用户名 / input nameemail typeemail value{formData.email} onChange{handleChange} placeholder邮箱 / input namepassword typepassword value{formData.password} onChange{handleChange} placeholder密码 / {/* 其他字段类似 */} /form ); }3.2 表单验证function FormWithValidation() { const [formData, setFormData] useState({ email: , password: }); const [errors, setErrors] useState({}); const validate () { const newErrors {}; if (!formData.email) { 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位; } setErrors(newErrors); return Object.keys(newErrors).length 0; }; const handleSubmit (e) { e.preventDefault(); if (validate()) { console.log(表单提交, formData); } }; const handleChange (e) { const { name, value } e.target; setFormData(prev ({ ...prev, [name]: value })); // 清除对应字段的错误 if (errors[name]) { setErrors(prev ({ ...prev, [name]: })); } }; return ( form onSubmit{handleSubmit} div input nameemail value{formData.email} onChange{handleChange} placeholder邮箱 / {errors.email span classNameerror{errors.email}/span} /div div input namepassword typepassword value{formData.password} onChange{handleChange} placeholder密码 / {errors.password span classNameerror{errors.password}/span} /div button typesubmit提交/button /form ); }3.3 表单提交处理function SubmitForm() { const [isSubmitting, setIsSubmitting] useState(false); const [submitStatus, setSubmitStatus] useState(null); const handleSubmit async (e) { e.preventDefault(); setIsSubmitting(true); setSubmitStatus(null); try { // 模拟 API 请求 await new Promise(resolve setTimeout(resolve, 2000)); setSubmitStatus({ type: success, message: 提交成功 }); // 重置表单 e.target.reset(); } catch (error) { setSubmitStatus({ type: error, message: 提交失败请重试 }); } finally { setIsSubmitting(false); } }; return ( form onSubmit{handleSubmit} {/* 表单字段 */} button typesubmit disabled{isSubmitting} {isSubmitting ? 提交中... : 提交} /button {submitStatus ( div className{submitStatus.type} {submitStatus.message} /div )} /form ); }四、表单库推荐4.1 React Hook Form (推荐)npminstallreact-hook-formimport { useForm } from react-hook-form; function ReactHookFormExample() { const { register, handleSubmit, formState: { errors, isSubmitting } } useForm(); const onSubmit async (data) { console.log(data); await new Promise(resolve setTimeout(resolve, 1000)); }; return ( form onSubmit{handleSubmit(onSubmit)} input {...register(username, { required: 用户名不能为空 })} placeholder用户名 / {errors.username span{errors.username.message}/span} input {...register(email, { required: 邮箱不能为空, pattern: { value: /\S\S\.\S/, message: 邮箱格式不正确 } })} placeholder邮箱 / {errors.email span{errors.email.message}/span} button typesubmit disabled{isSubmitting} {isSubmitting ? 提交中... : 提交} /button /form ); }4.2 Formiknpminstallformik yupimport { useFormik } from formik; import * as Yup from yup; const validationSchema Yup.object({ username: Yup.string().required(用户名不能为空), email: Yup.string().email(邮箱格式不正确).required(邮箱不能为空), password: Yup.string().min(6, 密码至少6位).required(密码不能为空) }); function FormikExample() { const formik useFormik({ initialValues: { username: , email: , password: }, validationSchema, onSubmit: async (values) { console.log(values); } }); return ( form onSubmit{formik.handleSubmit} input nameusername value{formik.values.username} onChange{formik.handleChange} onBlur{formik.handleBlur} / {formik.touched.username formik.errors.username ( div{formik.errors.username}/div )} {/* 其他字段类似 */} button typesubmit提交/button /form ); }五、React 19 表单新特性5.1 useActionStateimport { useActionState } from react; async function submitForm(prevState, formData) { const name formData.get(name); const email formData.get(email); // 模拟 API 调用 await new Promise(resolve setTimeout(resolve, 1000)); return { message: 提交成功${name} (${email}) }; } function ActionForm() { const [state, formAction, isPending] useActionState(submitForm, null); return ( form action{formAction} input namename placeholder姓名 required / input nameemail typeemail placeholder邮箱 required / button typesubmit disabled{isPending} {isPending ? 提交中... : 提交} /button {state?.message p{state.message}/p} /form ); }5.2 useFormStatusimport { useFormStatus } from react-dom; function SubmitButton() { const { pending } useFormStatus(); return ( button typesubmit disabled{pending} {pending ? 提交中... : 提交} /button ); } function FormWithStatus() { return ( form action{submitAction} input namedata / SubmitButton / /form ); }六、最佳实践6.1 表单组件封装function FormField({ label, name, type text, error, ...props }) { return ( div classNameform-field label htmlFor{name}{label}/label input id{name} name{name} type{type} className{error ? error : } {...props} / {error span classNameerror-message{error}/span} /div ); } // 使用 FormField label用户名 nameusername value{formData.username} onChange{handleChange} error{errors.username} /6.2 防抖处理function DebouncedSearch() { const [searchTerm, setSearchTerm] useState(); const [debouncedTerm, setDebouncedTerm] useState(); useEffect(() { const timer setTimeout(() { setDebouncedTerm(searchTerm); }, 500); return () clearTimeout(timer); }, [searchTerm]); return ( input value{searchTerm} onChange{(e) setSearchTerm(e.target.value)} placeholder搜索... / ); }七、练习题基础题实现一个登录表单邮箱 密码实现一个注册表单包含密码确认验证进阶题实现一个动态表单可以动态添加/删除输入框实现一个文件上传组件参考答案// 1. 动态表单 function DynamicForm() { const [fields, setFields] useState([{ id: Date.now(), value: }]); const addField () { setFields([...fields, { id: Date.now(), value: }]); }; const removeField (id) { setFields(fields.filter(field field.id ! id)); }; const updateField (id, value) { setFields(fields.map(field field.id id ? { ...field, value } : field )); }; return ( div {fields.map(field ( div key{field.id} input value{field.value} onChange{(e) updateField(field.id, e.target.value)} / button onClick{() removeField(field.id)}删除/button /div ))} button onClick{addField}添加字段/button /div ); }八、小结要点说明受控组件推荐使用状态统一管理非受控组件适用于简单场景或文件上传表单验证支持实时验证和提交验证表单库React Hook Form 性能最佳React 19使用 Action 简化表单处理核心要点优先使用受控组件选择合适的表单库处理好验证和错误提示注意用户体验加载状态、防抖
01-React基础入门——09-表单与受控组件
表单与受控组件一、受控组件 vs 非受控组件1.1 概念对比类型数据来源更新方式适用场景受控组件React stateonChange 事件更新大多数场景非受控组件DOM 自身ref 获取值简单表单、文件上传1.2 受控组件示例function ControlledInput() { const [value, setValue] useState(); const handleChange (e) { setValue(e.target.value); }; return ( input typetext value{value} onChange{handleChange} placeholder输入内容... / ); }1.3 非受控组件示例function UncontrolledInput() { const inputRef useRef(null); const handleSubmit () { alert(输入的值: inputRef.current.value); }; return ( div input typetext ref{inputRef} defaultValue默认值 / button onClick{handleSubmit}获取值/button /div ); }二、常见表单元素2.1 文本输入框function TextInput() { const [text, setText] useState(); return ( div label 用户名 input typetext value{text} onChange{(e) setText(e.target.value)} placeholder请输入用户名 / /label p当前输入{text}/p /div ); }2.2 文本域 (Textarea)function TextareaExample() { const [content, setContent] useState(); return ( div label 个人简介 textarea value{content} onChange{(e) setContent(e.target.value)} rows{5} cols{40} placeholder介绍一下自己... / /label p字符数{content.length}/p /div ); }2.3 单选框 (Radio)function RadioGroup() { const [gender, setGender] useState(); return ( div label input typeradio valuemale checked{gender male} onChange{(e) setGender(e.target.value)} / 男 /label label input typeradio valuefemale checked{gender female} onChange{(e) setGender(e.target.value)} / 女 /label p选择的性别{gender}/p /div ); }2.4 复选框 (Checkbox)function CheckboxExample() { const [isAgreed, setIsAgreed] useState(false); const [interests, setInterests] useState([]); const handleInterestChange (interest) { setInterests(prev prev.includes(interest) ? prev.filter(i i ! interest) : [...prev, interest] ); }; return ( div label input typecheckbox checked{isAgreed} onChange{(e) setIsAgreed(e.target.checked)} / 同意条款 /label div 兴趣爱好 {[阅读, 音乐, 运动].map(interest ( label key{interest} input typecheckbox value{interest} checked{interests.includes(interest)} onChange{() handleInterestChange(interest)} / {interest} /label ))} /div /div ); }2.5 下拉选择框 (Select)function SelectExample() { const [city, setCity] useState(); const [cities, setCities] useState([]); const handleMultipleChange (e) { const options Array.from(e.target.selectedOptions); setCities(options.map(opt opt.value)); }; return ( div {/* 单选 */} select value{city} onChange{(e) setCity(e.target.value)} option value请选择城市/option option valuebeijing北京/option option valueshanghai上海/option option valueguangzhou广州/option /select {/* 多选 */} select multiple value{cities} onChange{handleMultipleChange} option valuebeijing北京/option option valueshanghai上海/option option valueguangzhou广州/option /select /div ); }三、复杂表单处理3.1 表单状态管理function RegistrationForm() { const [formData, setFormData] useState({ username: , email: , password: , confirmPassword: , age: , gender: , agree: false }); const handleChange (e) { const { name, value, type, checked } e.target; setFormData(prev ({ ...prev, [name]: type checkbox ? checked : value })); }; return ( form input nameusername value{formData.username} onChange{handleChange} placeholder用户名 / input nameemail typeemail value{formData.email} onChange{handleChange} placeholder邮箱 / input namepassword typepassword value{formData.password} onChange{handleChange} placeholder密码 / {/* 其他字段类似 */} /form ); }3.2 表单验证function FormWithValidation() { const [formData, setFormData] useState({ email: , password: }); const [errors, setErrors] useState({}); const validate () { const newErrors {}; if (!formData.email) { 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位; } setErrors(newErrors); return Object.keys(newErrors).length 0; }; const handleSubmit (e) { e.preventDefault(); if (validate()) { console.log(表单提交, formData); } }; const handleChange (e) { const { name, value } e.target; setFormData(prev ({ ...prev, [name]: value })); // 清除对应字段的错误 if (errors[name]) { setErrors(prev ({ ...prev, [name]: })); } }; return ( form onSubmit{handleSubmit} div input nameemail value{formData.email} onChange{handleChange} placeholder邮箱 / {errors.email span classNameerror{errors.email}/span} /div div input namepassword typepassword value{formData.password} onChange{handleChange} placeholder密码 / {errors.password span classNameerror{errors.password}/span} /div button typesubmit提交/button /form ); }3.3 表单提交处理function SubmitForm() { const [isSubmitting, setIsSubmitting] useState(false); const [submitStatus, setSubmitStatus] useState(null); const handleSubmit async (e) { e.preventDefault(); setIsSubmitting(true); setSubmitStatus(null); try { // 模拟 API 请求 await new Promise(resolve setTimeout(resolve, 2000)); setSubmitStatus({ type: success, message: 提交成功 }); // 重置表单 e.target.reset(); } catch (error) { setSubmitStatus({ type: error, message: 提交失败请重试 }); } finally { setIsSubmitting(false); } }; return ( form onSubmit{handleSubmit} {/* 表单字段 */} button typesubmit disabled{isSubmitting} {isSubmitting ? 提交中... : 提交} /button {submitStatus ( div className{submitStatus.type} {submitStatus.message} /div )} /form ); }四、表单库推荐4.1 React Hook Form (推荐)npminstallreact-hook-formimport { useForm } from react-hook-form; function ReactHookFormExample() { const { register, handleSubmit, formState: { errors, isSubmitting } } useForm(); const onSubmit async (data) { console.log(data); await new Promise(resolve setTimeout(resolve, 1000)); }; return ( form onSubmit{handleSubmit(onSubmit)} input {...register(username, { required: 用户名不能为空 })} placeholder用户名 / {errors.username span{errors.username.message}/span} input {...register(email, { required: 邮箱不能为空, pattern: { value: /\S\S\.\S/, message: 邮箱格式不正确 } })} placeholder邮箱 / {errors.email span{errors.email.message}/span} button typesubmit disabled{isSubmitting} {isSubmitting ? 提交中... : 提交} /button /form ); }4.2 Formiknpminstallformik yupimport { useFormik } from formik; import * as Yup from yup; const validationSchema Yup.object({ username: Yup.string().required(用户名不能为空), email: Yup.string().email(邮箱格式不正确).required(邮箱不能为空), password: Yup.string().min(6, 密码至少6位).required(密码不能为空) }); function FormikExample() { const formik useFormik({ initialValues: { username: , email: , password: }, validationSchema, onSubmit: async (values) { console.log(values); } }); return ( form onSubmit{formik.handleSubmit} input nameusername value{formik.values.username} onChange{formik.handleChange} onBlur{formik.handleBlur} / {formik.touched.username formik.errors.username ( div{formik.errors.username}/div )} {/* 其他字段类似 */} button typesubmit提交/button /form ); }五、React 19 表单新特性5.1 useActionStateimport { useActionState } from react; async function submitForm(prevState, formData) { const name formData.get(name); const email formData.get(email); // 模拟 API 调用 await new Promise(resolve setTimeout(resolve, 1000)); return { message: 提交成功${name} (${email}) }; } function ActionForm() { const [state, formAction, isPending] useActionState(submitForm, null); return ( form action{formAction} input namename placeholder姓名 required / input nameemail typeemail placeholder邮箱 required / button typesubmit disabled{isPending} {isPending ? 提交中... : 提交} /button {state?.message p{state.message}/p} /form ); }5.2 useFormStatusimport { useFormStatus } from react-dom; function SubmitButton() { const { pending } useFormStatus(); return ( button typesubmit disabled{pending} {pending ? 提交中... : 提交} /button ); } function FormWithStatus() { return ( form action{submitAction} input namedata / SubmitButton / /form ); }六、最佳实践6.1 表单组件封装function FormField({ label, name, type text, error, ...props }) { return ( div classNameform-field label htmlFor{name}{label}/label input id{name} name{name} type{type} className{error ? error : } {...props} / {error span classNameerror-message{error}/span} /div ); } // 使用 FormField label用户名 nameusername value{formData.username} onChange{handleChange} error{errors.username} /6.2 防抖处理function DebouncedSearch() { const [searchTerm, setSearchTerm] useState(); const [debouncedTerm, setDebouncedTerm] useState(); useEffect(() { const timer setTimeout(() { setDebouncedTerm(searchTerm); }, 500); return () clearTimeout(timer); }, [searchTerm]); return ( input value{searchTerm} onChange{(e) setSearchTerm(e.target.value)} placeholder搜索... / ); }七、练习题基础题实现一个登录表单邮箱 密码实现一个注册表单包含密码确认验证进阶题实现一个动态表单可以动态添加/删除输入框实现一个文件上传组件参考答案// 1. 动态表单 function DynamicForm() { const [fields, setFields] useState([{ id: Date.now(), value: }]); const addField () { setFields([...fields, { id: Date.now(), value: }]); }; const removeField (id) { setFields(fields.filter(field field.id ! id)); }; const updateField (id, value) { setFields(fields.map(field field.id id ? { ...field, value } : field )); }; return ( div {fields.map(field ( div key{field.id} input value{field.value} onChange{(e) updateField(field.id, e.target.value)} / button onClick{() removeField(field.id)}删除/button /div ))} button onClick{addField}添加字段/button /div ); }八、小结要点说明受控组件推荐使用状态统一管理非受控组件适用于简单场景或文件上传表单验证支持实时验证和提交验证表单库React Hook Form 性能最佳React 19使用 Action 简化表单处理核心要点优先使用受控组件选择合适的表单库处理好验证和错误提示注意用户体验加载状态、防抖