react 表单管理
受控模式
严格来说,虽然从业务上来说这是一个表单,但从编码上来看 并没有用到 form!
其中共涉及到如下定义和维护的状态:
- 表单本身数据的状态 formData(由于使用了受控组件,因此需要为该字段提供默认值)
- 提交中状态 isPending
- 提交结果状态 submitResult
import _ from 'lodash';
import cn from '@/utils/cn';
import * as api from '../api';
import { useState } from 'react';
import { match } from 'ts-pattern';
type SubmitResult = { type: 'idle' | 'success' | 'error'; data: string };
type FormDataState = { uname: string; comment: string };
const initDate = {
submitResult: { type: 'idle', data: '' },
formData: { uname: '', comment: '' },
};
// React 18实现(受控来管理"表单")
export default function formDemo() {
const [isPending, setIsPending] = useState(false);
const [submitResult, setSubmitResult] = useState<SubmitResult>(_.cloneDeep(initDate.submitResult));
const [formData, setFormData] = useState<FormDataState>(_.cloneDeep(initDate.formData));
// 提交操作
const handleSubmit = async () => {
setIsPending(true);
const finalFormData = new FormData();
Object.entries(formData).forEach(([key, value]) => {
finalFormData.append(key, value);
});
const res = await api.submitComment(finalFormData);
setSubmitResult({ type: res.code === 0 ? 'success' : 'error', data: res.msg });
setIsPending(false);
};
// 重置操作
const handleReset = () => {
setFormData(_.cloneDeep(initDate.formData));
setSubmitResult(_.cloneDeep(initDate.submitResult));
};
return (
<div className="w-auto">
<div className={cn('flex flex-col gap-2', { 'opacity-50': isPending })}>
<div>
<label>昵称:</label>
<input name="uname" value={formData.uname} onChange={(e) => setFormData({ ...formData, uname: e.target.value })} className="border rounded px-2" />
</div>
<div>
<label>评论:</label>
<input name="comment" value={formData.comment} onChange={(e) => setFormData({ ...formData, comment: e.target.value })} className="border rounded px-2" />
</div>
<div className="flex gap-2">
<button className="bg-blue-400 text-white px-2 rounded w-20 p-1" onClick={handleSubmit}>
发表
</button>
<button className="bg-red-400 text-white px-2 rounded w-20 p-1" onClick={handleReset}>
重置
</button>
</div>
</div>
<hr className="my-2" />
{match(isPending)
.with(true, () => <span className="text-yellow-400 text-xs">提交中...</span>)
.otherwise(() =>
match(submitResult.type)
.with('error', () => <span className="text-red-400 text-xs">{submitResult.data}</span>)
.with('success', () => <span className="text-green-400 text-xs">{submitResult.data}</span>)
.otherwise(() => null),
)}
</div>
);
}
非受控模式
在受控下我们可以看到,要完成一个表单的基本能力,需要定义一堆的状态来维护。这显然有点繁琐,尤其是遇到大量的表单字段的时候!
那我们为何不考虑非受控 原生表单呢?
这种模式下,你无需关心表单本身的字段状态,仅定义 isPending 和 submitResult 即可!
import _ from 'lodash';
import cn from '@/utils/cn';
import * as api from '../api';
import { useState } from 'react';
import { match } from 'ts-pattern';
type SubmitResult = { type: 'idle' | 'success' | 'error'; data: string };
const initDate = {
submitResult: { type: 'idle', data: '' },
formData: { uname: '', comment: '' },
};
// React 18实现(通过非受控来管理"表单")
export default function formDemo() {
const [isPending, setIsPending] = useState(false);
const [submitResult, setSubmitResult] = useState<SubmitResult>(_.cloneDeep(initDate.submitResult));
// 提交操作
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); // 阻止默认行为(页面重定向)
setIsPending(true);
const formData = new FormData(e.target as HTMLFormElement); // 通过非受控模式获取表单数据
const res = await api.submitComment(formData);
setSubmitResult({ type: res.code === 0 ? 'success' : 'error', data: res.msg });
setIsPending(false);
};
// 监听重置操作
const onReset = () => setSubmitResult(_.cloneDeep(initDate.submitResult));
return (
<div className="w-auto">
<form onSubmit={handleSubmit} className={cn('flex flex-col gap-2', { 'opacity-50': isPending })} onReset={onReset}>
<div>
<label>昵称:</label>
<input name="uname" className="border rounded px-2" />
</div>
<div>
<label>评论:</label>
<input name="comment" className="border rounded px-2" />
</div>
<div className="flex gap-2">
<button className="bg-blue-400 text-white px-2 rounded w-20 p-1" type='submit'>
发表
</button>
<button className="bg-red-400 text-white px-2 rounded w-20 p-1" type='reset'>
重置
</button>
</div>
</form>
<hr className="my-2" />
{match(isPending)
.with(true, () => <span className="text-yellow-400 text-xs">提交中...</span>)
.otherwise(() =>
match(submitResult.type)
.with('error', () => <span className="text-red-400 text-xs">{submitResult.data}</span>)
.with('success', () => <span className="text-green-400 text-xs">{submitResult.data}</span>)
.otherwise(() => null),
)}
</div>
);
}
react19非受控优化版
利用新的 hook useActionState,完美实现一个 state 都不需要定义
import _ from 'lodash';
import cn from '@/utils/cn';
import * as api from '../api';
import { match } from 'ts-pattern';
import { useActionState, startTransition } from 'react';
type SubmitResult = { type: 'idle' | 'success' | 'error'; data: string };
const initSubmitResult: SubmitResult = { type: 'idle', data: '' };
// React 19实现(通过useActionState结合非受控来管理"表单")
export default function formDemo() {
const handleSubmit = async (_: SubmitResult, formData: FormData): Promise<SubmitResult> => {
if (formData.has('reset')) return initSubmitResult;
const res = await api.submitComment(formData);
return { type: res.code === 0 ? 'success' : 'error', data: res.msg };
};
const [submitResult, formAction, isPending] = useActionState(handleSubmit, initSubmitResult);
const onReset = () => {
// 因为重置不会调用formAction,所以我们要手动调用,便于重置submitResult
startTransition(() => {
const formData = new FormData();
formData.append('reset', '');
formAction(formData);
});
};
return (
<div className="w-auto">
<form action={formAction} className={cn('flex flex-col gap-2', { 'opacity-50': isPending })} onReset={onReset}>
<div>
<label>昵称:</label>
<input name="uname" className="border rounded px-2" />
</div>
<div>
<label>评论:</label>
<input name="comment" className="border rounded px-2" />
</div>
<div className="flex gap-2">
<button className="bg-blue-400 text-white px-2 rounded w-20 p-1" type="submit">
发表
</button>
<button className="bg-red-400 text-white px-2 rounded w-20 p-1" type="reset">
重置
</button>
</div>
</form>
<hr className="my-2" />
{match(isPending)
.with(true, () => <span className="text-yellow-400 text-xs">提交中...</span>)
.otherwise(() =>
match(submitResult.type)
.with('error', () => <span className="text-red-400 text-xs">{submitResult.data}</span>)
.with('success', () => <span className="text-green-400 text-xs">{submitResult.data}</span>)
.otherwise(() => null),
)}
</div>
);
}
注意:useActionState的提交完成后会导致表单数据被重置,这是此钩子的默认行为,不习惯的可以参考这里
总结
该文通过三个代码示例,循序渐进地展示了在React中处理表单的三种不同模式,重点说明了从传统方式到React 19新特性的演进过程。
-
受控模式 (Controlled Component)
- 核心思想:使用React的
useState来全面管理表单的所有状态,包括每个输入框的值 (formData)、提交中的状态 (isPending) 和提交结果 (submitResult)。 - 缺点:代码相对繁琐,尤其是当表单字段很多时,需要定义和维护大量的状态。
- 核心思想:使用React的
-
非受控模式 (Uncontrolled Component)
- 核心思想:利用原生HTML的
<form>特性,将表单数据交由DOM管理。React不再追踪每个输入框的值,只在提交时通过new FormData(formElement)一次性获取所有数据。 - 优点:显著简化了代码,开发者只需关心提交中 (
isPending) 和提交结果 (submitResult) 的状态,无需为每个字段创建state。
- 核心思想:利用原生HTML的
-
React 19 非受控优化版 (Optimized with
useActionState)- 核心思想:利用React 19 推出的新Hook
useActionState来进一步简化非受控表单。 - 优点:达到了极致的简洁。
useActionState这一个Hook就统一管理了 提交结果、加载状态(isPending)、以及表单动作(action),完全无需手动创建任何useState。代码变得更加声明式,可读性更高。 - 实现方式:将提交逻辑封装成一个action函数,并将其传递给
useActionState,然后将返回的formAction绑定到<form>的action属性上。
- 核心思想:利用React 19 推出的新Hook
文件清晰地展示了React表单处理模式的演进:从需要大量手动状态管理的“受控模式”,到简化数据管理的“非受控模式”,最终到借助React 19 useActionState 实现几乎零手动状态管理的“优化非受控模式”,体现了React框架在提升开发效率和减少样板代码方面的持续努力。

浙公网安备 33010602011771号