看完这篇,你能用同一张登录表单分别写出 React Hook Form 和 Formik 两版实现,并知道什么场景该选哪个。
第一步:先搞清楚两者的设计思路差在哪
这是选型的根本,代码只是表象。
- React Hook Form(下称 RHF):走「非受控 + ref」路线。输入框的值由 DOM 自己管,RHF 只在需要时通过 ref 读,并用订阅机制只让用到该字段的组件重渲染。
- Formik:走「受控 + state」路线。所有字段值都放在 state 里,每次输入触发一次 setState,整个表单组件树默认跟着重渲染。
直接后果:字段越多、表单项越复杂,Formik 的重渲染开销越明显;RHF 基本恒定。反过来,Formik 的值是显式的 state,调试时打印 values 一目了然,心智负担低。
注意:Formik 官方仓库已进入维护模式,新特性很少;RHF 仍在活跃迭代。新项目做长期规划时要把这点算进去。
第二步:装依赖
npm i react-hook-form
# 方案 B
npm i formik yup
RHF 想用 schema 校验时再补:
npm i @hookform/resolvers zod
第三步:用 React Hook Form 写登录表单
import { useForm } from 'react-hook-form';
export default function LoginForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm({ defaultValues: { email: '', password: '' } });
const onSubmit = async (data) => {
await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register('email', {
required: '请输入邮箱',
pattern: { value: /^\S+@\S+$/, message: '邮箱格式不对' },
})}
/>
{errors.email && <p>{errors.email.message}</p>}
<input
type="password"
{...register('password', {
required: '请输入密码',
minLength: { value: 6, message: '至少 6 位' },
})}
/>
{errors.password && <p>{errors.password.message}</p>}
<button disabled={isSubmitting}>登录</button>
</form>
);
}
关键就三点:register 负责把 input 挂上去,handleSubmit 负责校验通过后再调你的回调,formState 是订阅式的——你读了哪些属性(如 errors),才订阅哪些更新。
注意:用了 register 就不要再给同一个 input 手写 value / onChange,两者会打架。确实要用受控组件(比如 antd、MUI 的输入框),改用 Controller 包一层。
第四步:用 Formik 写同一张表单
import { useFormik } from 'formik';
import * as Yup from 'yup';
const schema = Yup.object({
email: Yup.string().email('邮箱格式不对').required('请输入邮箱'),
password: Yup.string().min(6, '至少 6 位').required('请输入密码'),
});
export default function LoginForm() {
const formik = useFormik({
initialValues: { email: '', password: '' },
validationSchema: schema,
onSubmit: async (values) => {
await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values),
});
},
});
return (
<form onSubmit={formik.handleSubmit}>
<input name="email" {...formik.getFieldProps('email')} />
{formik.touched.email && formik.errors.email && <p>{formik.errors.email}</p>}
<input type="password" name="password" {...formik.getFieldProps('password')} />
{formik.touched.password && formik.errors.password && <p>{formik.errors.password}</p>}
<button type="submit" disabled={formik.isSubmitting}>登录</button>
</form>
);
}
getFieldProps('email') 一步到位帮你绑好 value / onChange / onBlur,剩下的全靠 formik 对象。想省掉 Yup,也可以直接传 validate 函数手写校验。
注意:Formik 默认每次输入都跑一遍 validationSchema。表单字段超过十几个时,把 validateOnChange 关掉、只留 validateOnBlur: true,体感会好很多。
第五步:动态字段和 UI 库怎么接
- 动态增减字段:RHF 用
useFieldArray;Formik 用 <FieldArray>。两者都够用,RHF 的 API 稍简洁。
- 接第三方组件库:RHF 用
<Controller>;Formik 用 setFieldValue + 手动传 value。
- 跨字段联动:RHF 用
watch 读值再 setValue;Formik 直接读 formik.values.xxx。
注意:RHF 的 watch 会让整个表单随该字段重渲染,字段多时优先用 useWatch 精确订阅单个字段。
第六步:到底怎么选
- 表单字段多、有大量动态列表、性能敏感 → RHF。
- 团队已经在用 Formik、存量代码多、迁移成本高于收益 → 继续 Formik,别为了尝鲜重写。
- 新项目、无历史包袱 → RHF,体积更小、重渲染更少、维护更活跃。
衡量性能别靠感觉:打开 React DevTools 的 Profiler,勾上「Highlight updates when components render」,然后在输入框里打几个字,看哪些块在闪。
小结
- RHF 是非受控 + ref + 订阅,Formik 是受控 + state,这是所有差异的源头。
- RHF 靠
register / handleSubmit / formState 三件套,Formik 靠 useFormik 返回的对象加 Yup schema。
register 别和受控写法混用;Formik 字段多时关掉 validateOnChange。
- 选型看存量与规模,不看热度;性能差异用 Profiler 实测,别猜。