目录
- [React Hook Form 简介](#React Hook Form 简介 "#react-hook-form-%E7%AE%80%E4%BB%8B")
- [核心 Hooks 介绍](#核心 Hooks 介绍 "#%E6%A0%B8%E5%BF%83-hooks-%E4%BB%8B%E7%BB%8D")
- 为什么需要状态下沉
- 状态下沉最佳实践
- 完整代码示例
- 优势分析
- 适用场景
- 总结
React Hook Form 简介
React Hook Form 是一个高性能、轻量级的 React 表单库,它采用非受控组件的方式,极大地减少了表单输入时的重新渲染次数。
核心特点:
- 性能卓越:使用非受控组件,最小化重新渲染
- 轻量级:体积小,无额外依赖
- TypeScript 友好:完整的类型推导支持
- 验证集成:内置支持 Zod、Yup 等验证库
- 易于使用:API 简洁直观
核心 Hooks
1. useForm - 表单核心 Hook
tsx
const {
register, // 注册输入框
handleSubmit, // 处理表单提交
control, // 控制器对象
watch, // 监听表单值
formState: { errors, isDirty, isValid },
setValue, // 手动设置值
reset, // 重置表单
} = useForm({
resolver: zodResolver(schema),
defaultValues: { ... }
});
2. useFormContext - 访问表单 Context
tsx
// 在子组件中,无需从 props 接收
const {
register,
formState: { errors },
} = useFormContext();
3. useFieldArray - 动态数组管理
tsx
const { fields, append, remove } = useFieldArray({
control,
name: 'items',
});
4. useWatch - 监听特定字段(性能优化)
tsx
const items = useWatch({
control,
name: 'items',
});
使用策略
策略 1:非受控为主,受控为辅
- 原生 input 使用
register - 第三方组件使用
Controller
策略 2:状态集中管理
- 在顶层创建表单实例
- 通过 Context 共享状态
策略 3:按需渲染
- 使用
useWatch避免不必要的渲染 - 使用
FormProvider优化性能
为什么需要状态下沉
在大型表单中,传统做法会遇到以下问题:
1. Props Drilling(属性透传)
tsx
// ❌ 问题:需要层层传递 props
function OrderForm() {
const { register, errors, control } = useForm();
return (
<CustomerInfo register={register} errors={errors} />
<ShippingInfo register={register} errors={errors} />
<OrderItems control={control} register={register} errors={errors} />
);
}
缺点:
- 组件层级深时,props 需要层层传递
- 子组件依赖父组件,耦合度高
- 难以复用
- 代码冗余
2. 父组件频繁渲染
tsx
// ❌ 问题:任何字段变化都会导致整个表单重新渲染
function OrderForm() {
const formData = watch(); // 监听整个表单
return <div>{/* 整个组件都会重新渲染 */}</div>;
}
3. 状态分散
tsx
// ❌ 问题:状态分散在各个组件中
function CustomerInfo() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
// ...
}
状态下沉最佳实践
核心思想
"状态提升 + Context 下沉":
- 状态提升:在顶层创建和管理表单状态
- Context 下沉:通过 React Context 将表单方法下沉到子组件
- 按需获取:子组件只获取需要的表单方法
- 按需渲染 :使用
useWatch避免不必要的渲染
实践步骤
步骤 1:在顶层创建表单实例
tsx
export default function OrderForm() {
// ✅ 在顶层统一管理表单状态
const methods = useForm<OrderFormValues>({
resolver: zodResolver(orderSchema),
defaultValues: {
customer: { name: '', email: '', phone: '' },
shipping: { address: '', city: '', zipCode: '' },
items: [{ productId: '', quantity: 1, price: 0 }],
},
});
const onSubmit = (data: OrderFormValues) => {
console.log('订单数据:', data);
};
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>{/* 子组件 */}</form>
</FormProvider>
);
}
关键点:
- 在顶层创建
methods对象 - 使用
FormProvider包裹整个表单 - 将
methods展开传递给FormProvider
步骤 2:在子组件中通过 Context 获取表单方法
tsx
function CustomerInfo() {
// ✅ 使用 useFormContext 获取表单方法,无需 props
const {
register,
formState: { errors },
} = useFormContext();
return (
<div>
<input {...register('customer.name')} />
{errors.customer?.name && <p>{errors.customer.name.message}</p>}
<input {...register('customer.email')} />
{errors.customer?.email && <p>{errors.customer.email.message}</p>}
</div>
);
}
优势:
- 无需从 props 接收任何参数
- 组件完全独立,可复用
- 代码简洁
步骤 3:处理动态数组(useFieldArray)
tsx
function OrderItems() {
// ✅ 从 Context 获取 control 和 register
const {
control,
register,
formState: { errors },
} = useFormContext();
// ✅ useFieldArray 需要 control
const { fields, append, remove } = useFieldArray({
control,
name: 'items',
});
const addItem = () => {
append({ productId: '', quantity: 1, price: 0 });
};
return (
<div>
{fields.map((field, index) => (
<div key={field.id}>
<input {...register(`items.${index}.productId`)} />
<input
{...register(`items.${index}.quantity`, { valueAsNumber: true })}
/>
<input
{...register(`items.${index}.price`, { valueAsNumber: true })}
/>
<button type="button" onClick={() => remove(index)}>
删除
</button>
</div>
))}
<button type="button" onClick={addItem}>
添加商品
</button>
</div>
);
}
关键点:
useFieldArray需要control对象- 通过
useFormContext()获取control - 无需从 props 传递
步骤 4:优化性能(useWatch)
tsx
function OrderSummary() {
// ✅ 从 Context 获取 control
const { control } = useFormContext();
// ✅ 使用 useWatch 监听特定字段,避免父组件渲染
const items = useWatch({
control,
name: 'items',
});
const calculateTotal = () => {
if (!items || items.length === 0) return 0;
return items.reduce((total, item) => {
return total + (item.quantity || 0) * (item.price || 0);
}, 0);
};
const total = calculateTotal();
return (
<div>
<h3>订单总计</h3>
<div>¥{total.toFixed(2)}</div>
</div>
);
}
优势:
- 只有
OrderSummary组件在items变化时重新渲染 - 父组件
OrderForm不会重新渲染 - 性能更好
步骤 5:处理表单操作
tsx
function FormActions() {
// ✅ 从 Context 获取表单状态
const {
formState: { isSubmitting, isDirty, isValid },
} = useFormContext();
return (
<div>
<button type="button" onClick={() => window.location.reload()}>
重置
</button>
<button type="submit" disabled={!isDirty || !isValid || isSubmitting}>
{isSubmitting ? '提交中...' : '提交订单'}
</button>
</div>
);
}
完整代码示例
1. Schema 定义
tsx
import { z } from 'zod';
const orderSchema = z.object({
customer: z.object({
name: z.string().min(2, '姓名至少需要2个字符'),
email: z.string().email('请输入有效的邮箱地址'),
phone: z.string().min(6, '电话至少需要6位数字'),
}),
shipping: z.object({
address: z.string().min(5, '地址至少需要5个字符'),
city: z.string().min(2, '城市至少需要2个字符'),
zipCode: z.string().min(5, '邮编至少需要5位'),
}),
items: z
.array(
z.object({
productId: z.string().min(1, '请选择产品'),
quantity: z.number().min(1, '数量至少为1'),
price: z.number().min(0, '价格不能为负数'),
})
)
.min(1, '至少需要添加一个商品'),
});
type OrderFormValues = z.infer<typeof orderSchema>;
2. CustomerInfo 组件
tsx
function CustomerInfo() {
const {
register,
formState: { errors },
} = useFormContext();
return (
<div className="space-y-4 rounded-lg border p-4">
<h3 className="text-lg font-semibold">客户信息</h3>
<div>
<label className="mb-1 block text-sm font-medium">姓名 *</label>
<input
{...register('customer.name')}
className="w-full rounded-md border px-3 py-2"
placeholder="请输入姓名"
/>
{errors.customer?.name && (
<p className="text-sm text-red-500">{errors.customer.name.message}</p>
)}
</div>
<div>
<label className="mb-1 block text-sm font-medium">邮箱 *</label>
<input
{...register('customer.email')}
className="w-full rounded-md border px-3 py-2"
placeholder="请输入邮箱"
/>
{errors.customer?.email && (
<p className="text-sm text-red-500">
{errors.customer.email.message}
</p>
)}
</div>
<div>
<label className="mb-1 block text-sm font-medium">电话 *</label>
<input
{...register('customer.phone')}
className="w-full rounded-md border px-3 py-2"
placeholder="请输入电话"
/>
{errors.customer?.phone && (
<p className="text-sm text-red-500">
{errors.customer.phone.message}
</p>
)}
</div>
</div>
);
}
3. ShippingInfo 组件
tsx
function ShippingInfo() {
const {
register,
formState: { errors },
} = useFormContext();
return (
<div className="space-y-4 rounded-lg border p-4">
<h3 className="text-lg font-semibold">配送信息</h3>
<div>
<label className="mb-1 block text-sm font-medium">地址 *</label>
<input
{...register('shipping.address')}
className="w-full rounded-md border px-3 py-2"
placeholder="请输入配送地址"
/>
{errors.shipping?.address && (
<p className="text-sm text-red-500">
{errors.shipping.address.message}
</p>
)}
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="mb-1 block text-sm font-medium">城市 *</label>
<input
{...register('shipping.city')}
className="w-full rounded-md border px-3 py-2"
placeholder="城市"
/>
{errors.shipping?.city && (
<p className="text-sm text-red-500">
{errors.shipping.city.message}
</p>
)}
</div>
<div>
<label className="mb-1 block text-sm font-medium">邮编 *</label>
<input
{...register('shipping.zipCode')}
className="w-full rounded-md border px-3 py-2"
placeholder="邮编"
/>
{errors.shipping?.zipCode && (
<p className="text-sm text-red-500">
{errors.shipping.zipCode.message}
</p>
)}
</div>
</div>
</div>
);
}
4. OrderItems 组件(动态数组)
tsx
function OrderItems() {
const {
control,
register,
formState: { errors },
} = useFormContext();
const { fields, append, remove } = useFieldArray({
control,
name: 'items',
});
const addItem = () => {
append({ productId: '', quantity: 1, price: 0 });
};
const products = [
{ id: 'p1', name: '笔记本电脑', price: 5999 },
{ id: 'p2', name: '机械键盘', price: 299 },
{ id: 'p3', name: '无线鼠标', price: 99 },
{ id: 'p4', name: '显示器', price: 1599 },
];
return (
<div className="space-y-4 rounded-lg border p-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">订单商品</h3>
<button
type="button"
onClick={addItem}
className="rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
添加商品
</button>
</div>
<div className="space-y-3">
{fields.map((field, index) => (
<div
key={field.id}
className="grid grid-cols-12 gap-3 rounded-md border p-3"
>
<div className="col-span-5">
<select
{...register(`items.${index}.productId`)}
className="w-full rounded-md border px-3 py-2"
defaultValue=""
>
<option value="" disabled>
选择产品
</option>
{products.map(product => (
<option key={product.id} value={product.id}>
{product.name} - ¥{product.price}
</option>
))}
</select>
{errors.items?.[index]?.productId && (
<p className="text-sm text-red-500">
{errors.items[index].productId.message}
</p>
)}
</div>
<div className="col-span-3">
<input
type="number"
{...register(`items.${index}.quantity`, {
valueAsNumber: true,
})}
className="w-full rounded-md border px-3 py-2"
placeholder="数量"
/>
{errors.items?.[index]?.quantity && (
<p className="text-sm text-red-500">
{errors.items[index].quantity.message}
</p>
)}
</div>
<div className="col-span-3">
<input
type="number"
step="0.01"
{...register(`items.${index}.price`, { valueAsNumber: true })}
className="w-full rounded-md border px-3 py-2"
placeholder="单价"
/>
{errors.items?.[index]?.price && (
<p className="text-sm text-red-500">
{errors.items[index].price.message}
</p>
)}
</div>
<div className="col-span-1 flex items-center">
<button
type="button"
onClick={() => remove(index)}
className="rounded-md bg-red-600 px-2 py-1 text-white hover:bg-red-700"
>
删除
</button>
</div>
</div>
))}
{errors.items && (
<p className="text-sm text-red-500">{errors.items.message}</p>
)}
</div>
</div>
);
}
5. OrderSummary 组件(性能优化)
tsx
function OrderSummary() {
const { control } = useFormContext();
const items = useWatch({
control,
name: 'items',
});
const calculateTotal = () => {
if (!items || items.length === 0) return 0;
return items.reduce((total, item) => {
return total + (item.quantity || 0) * (item.price || 0);
}, 0);
};
const total = calculateTotal();
return (
<div className="rounded-lg border p-4">
<h3 className="text-lg font-semibold">订单总计</h3>
<div className="mt-4 text-2xl font-bold text-green-600">
¥{total.toFixed(2)}
</div>
<p className="text-sm text-gray-500">实时计算,避免父组件重复渲染</p>
</div>
);
}
6. FormActions 组件
tsx
function FormActions() {
const {
formState: { isSubmitting, isDirty, isValid },
} = useFormContext();
return (
<div className="flex justify-end space-x-4">
<button
type="button"
onClick={() => window.location.reload()}
className="rounded-md border px-6 py-2 hover:bg-gray-50"
>
重置
</button>
<button
type="submit"
disabled={!isDirty || !isValid || isSubmitting}
className="rounded-md bg-green-600 px-6 py-2 text-white hover:bg-green-700 disabled:opacity-50"
>
{isSubmitting ? '提交中...' : '提交订单'}
</button>
</div>
);
}
7. 顶层容器
tsx
export default function OrderForm() {
const methods = useForm<OrderFormValues>({
resolver: zodResolver(orderSchema),
defaultValues: {
customer: {
name: '',
email: '',
phone: '',
},
shipping: {
address: '',
city: '',
zipCode: '',
},
items: [{ productId: '', quantity: 1, price: 0 }],
},
});
const onSubmit = (data: OrderFormValues) => {
console.log('订单数据:', data);
alert('订单提交成功!请查看控制台');
};
return (
<div className="mx-auto max-w-4xl p-6">
<h1 className="mb-6 text-3xl font-bold">订单表单(状态下沉最佳实践)</h1>
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)} className="space-y-6">
<CustomerInfo />
<ShippingInfo />
<OrderItems />
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
<div className="md:col-span-2">{/* 其他内容 */}</div>
<div>
<OrderSummary />
</div>
</div>
<FormActions />
</form>
</FormProvider>
</div>
);
}
使用方式
在 App.tsx 中引入使用:
tsx
import OrderForm from './FormStateSinkDemo';
function App() {
return (
<div className="App">
<OrderForm />
</div>
);
}
export default App;
优势分析
1. 无需 Props Drilling
传统方式:
tsx
<CustomerInfo register={register} errors={errors} control={control} />
状态下沉:
tsx
<CustomerInfo /> // 无需 props
好处:
- 代码更简洁
- 组件层级深时优势明显
- 减少维护成本
2. 组件完全独立
tsx
function CustomerInfo() {
const {
register,
formState: { errors },
} = useFormContext();
// 组件自包含,不依赖父组件
}
好处:
- 组件可复用
- 可单独测试
- 易于维护
3. 性能优化
tsx
function OrderSummary() {
const { control } = useFormContext();
const items = useWatch({
control,
name: 'items', // 只监听这个字段
});
// ✅ 只有这个组件在 items 变化时重新渲染
}
对比:
watch():监听整个表单,任何字段变化都会触发渲染useWatch({ name: 'items' }):只监听指定字段
4. 状态集中管理
tsx
const methods = useForm({
resolver: zodResolver(orderSchema),
defaultValues: {
/* 所有默认值 */
},
});
好处:
- 状态一目了然
- 易于调试
- 验证规则集中定义
5. 易于扩展
tsx
// 添加新组件,无需修改父组件
<FormProvider {...methods}>
<CustomerInfo />
<ShippingInfo />
<OrderItems />
<PaymentInfo /> {/* 新增 */}
<OrderSummary />
</FormProvider>
适用场景
适合使用:
- ✅ 大型表单(多字段、多部分)
- ✅ 表单需要拆分成多个子组件
- ✅ 组件层级较深(3 层以上)
- ✅ 需要避免 Props Drilling
- ✅ 子组件需要复用
- ✅ 表单字段之间有联动
- ✅ 需要性能优化
不适合使用:
- ❌ 小型表单(少于 5 个字段)
- ❌ 简单的登录/注册表单
- ❌ 组件层级只有 1-2 层
- ❌ 一次性使用的简单表单
常见的疑问
Q1:这种方式会不会有性能问题?
不会! 相反,性能更好:
useFormContext只是读取 Context,开销很小useWatch可以精确控制渲染范围- 避免了父组件不必要的重新渲染
Q2:组件复用时会不会有冲突?
不会! 每个表单实例是独立的:
tsx
function Page() {
return (
<>
<FormProvider {...methods1}>
<CustomerInfo /> {/* 使用 methods1 */}
</FormProvider>
<FormProvider {...methods2}>
<CustomerInfo /> {/* 使用 methods2 */}
</FormProvider>
</>
);
}
Q3:useFormContext 和 React 的 useContext 一样吗?
这是一个非常好的问题!useFormContext 和 React 的 useContext 在底层机制上是一样的,但在实际使用中有重要区别。
相同点
- 底层机制相同:
tsx
// useFormContext 内部实现(简化版)
const useFormContext = () => {
return useContext(FormContext); // 实际就是 useContext
};
- 都会触发重新渲染:
tsx
// 当 Context 值变化时,所有使用 useContext/useFormContext 的组件都会重新渲染
const contextValue = useContext(MyContext); // 变化时触发渲染
const formMethods = useFormContext(); // 变化时也会触发渲染
关键区别
1. React Context 的问题
tsx
// ❌ 传统 Context 问题演示
const DataContext = createContext({ user: null, setUser: () => {} });
function Parent() {
const [user, setUser] = useState(null);
// 整个对象作为 Context 值
const contextValue = { user, setUser };
return (
<DataContext.Provider value={contextValue}>
<ChildA /> {/* user 变化时,所有子组件都重新渲染 */}
<ChildB />
<ChildC />
</DataContext.Provider>
);
}
function ChildA() {
const { user } = useContext(DataContext);
console.log('ChildA 渲染'); // user 变化时也会打印
return <div>{user?.name}</div>;
}
2. React Hook Form 的优化
tsx
// ✅ useFormContext 的优化
function OrderForm() {
const methods = useForm({
defaultValues: {
name: '',
email: '',
items: [],
},
});
// methods 对象是稳定的引用(useForm 内部做了优化)
return (
<FormProvider {...methods}>
<CustomerInfo /> {/* 不会因为其他字段变化而重新渲染 */}
<OrderItems /> {/* 只有 useFieldArray 使用的字段变化才可能影响 */}
<OrderSummary /> {/* 只有 useWatch 监听的字段变化才重新渲染 */}
</FormProvider>
);
}
function CustomerInfo() {
const { register } = useFormContext();
console.log('CustomerInfo 渲染'); // 只在自身相关变化时才打印
return <input {...register('name')} />;
}
function OrderSummary() {
const { control } = useFormContext();
const items = useWatch({ control, name: 'items' }); // 精确监听
console.log('OrderSummary 渲染'); // 只有 items 变化时才打印
return <div>总计: {items.length}</div>;
}
性能优化的关键
1. 稳定的 methods 对象
tsx
// React Hook Form 内部优化(简化版)
function useForm() {
const [state, setState] = useState(initialState);
// 返回的对象引用是稳定的
const methods = useMemo(
() => ({
register: () => {
/* ... */
},
handleSubmit: () => {
/* ... */
},
// 注意:这里的方法不直接包含状态值
}),
[]
);
return methods;
}
2. 精确的状态访问
tsx
// ❌ 传统方式:监听整个表单
function BadSummary() {
const { watch } = useFormContext();
const formData = watch(); // 监听所有字段
return <div>总计: {formData.items.length}</div>; // 任何字段变化都会触发渲染
}
// ✅ 优化方式:精确监听
function GoodSummary() {
const { control } = useFormContext();
const items = useWatch({ control, name: 'items' }); // 只监听 items
return <div>总计: {items.length}</div>; // 只有 items 变化才触发渲染
}
实际对比测试
tsx
// 测试代码
function TestForm() {
const methods = useForm();
console.log('1. Form 组件渲染');
return (
<FormProvider {...methods}>
<TestField1 />
<TestField2 />
<TestWatcher />
</FormProvider>
);
}
function TestField1() {
const { register } = useFormContext();
console.log('2. Field1 渲染');
return <input {...register('field1')} placeholder="字段1" />;
}
function TestField2() {
const { register } = useFormContext();
console.log('3. Field2 渲染');
return <input {...register('field2')} placeholder="字段2" />;
}
function TestWatcher() {
const { control } = useFormContext();
const field1 = useWatch({ control, name: 'field1' });
console.log('4. Watcher 渲染,field1值:', field1);
return <div>field1: {field1}</div>;
}
// 操作结果:
// 1. 初始渲染:打印 1,2,3,4
// 2. 修改 field1:只打印 4
// 3. 修改 field2:只打印 2(因为 TestField2 直接使用 register)
总结
| 特性 | React Context | useFormContext |
|---|---|---|
| 底层机制 | 相同 | 相同 |
| 默认行为 | Context 值变化 → 所有消费者重新渲染 | methods 对象稳定 → 减少不必要渲染 |
| 精确控制 | 需要手动拆分 Context | 内置 useWatch 精确监听 |
| 性能优化 | 需要开发者自行优化 | React Hook Form 内置优化 |
| 使用复杂度 | 需要理解 Context 优化技巧 | 开箱即用 |
关键在于 :React Hook Form 通过稳定的 methods 对象和 useWatch 精确监听,实现了比原生 Context 更好的性能表现!
Q4:如何测试子组件?
模拟 Context:
tsx
import { FormProvider, useForm } from 'react-hook-form';
function TestWrapper() {
const methods = useForm({
defaultValues: { customer: { name: '测试' } },
});
return (
<FormProvider {...methods}>
<CustomerInfo />
</FormProvider>
);
}
Q5:useWatch 和 watch 有什么区别?
tsx
// watch() - 在 render 中调用,导致组件重新渲染
function Component() {
const { watch } = useFormContext();
const value = watch('field');
return <div>{value}</div>;
}
// useWatch() - Hook,只在值变化时渲染
function Component() {
const { control } = useFormContext();
const value = useWatch({ control, name: 'field' });
return <div>{value}</div>;
}
区别:
watch():在 render 中调用,任何字段变化都会触发渲染useWatch():作为 Hook 使用,可以更精确地控制依赖
与其他方案对比
vs Props Drilling
| 特点 | Props Drilling | 状态下沉 |
|---|---|---|
| 代码简洁度 | ❌ 大量 props | ✅ 无需 props |
| 组件独立性 | ❌ 依赖父组件 | ✅ 完全独立 |
| 组件复用 | ❌ 难以复用 | ✅ 易于复用 |
| 层级深浅 | ❌ 越深越复杂 | ✅ 不受影响 |
| 维护成本 | ❌ 高 | ✅ 低 |
vs Redux/状态管理库
| 特点 | Redux | 状态下沉 |
|---|---|---|
| 依赖 | ❌ 需要额外库 | ✅ React 自带 |
| 配置复杂度 | ❌ 复杂 | ✅ 简单 |
| 表单专用性 | ❌ 通用方案 | ✅ 专为表单设计 |
| 学习成本 | ❌ 较高 | ✅ 低 |
| 性能 | ⚠️ 需要优化 | ✅ 内置优化 |
vs 传统受控组件
| 特点 | 受控组件 | 状态下沉 |
|---|---|---|
| 渲染次数 | ❌ 每次输入都渲染 | ✅ 最小化渲染 |
| 性能 | ❌ 较差 | ✅ 优秀 |
| 代码量 | ❌ 较多 | ✅ 简洁 |
| 验证 | ⚠️ 手动实现 | ✅ 内置支持 |
最佳实践总结
✅ 应该做的
-
在顶层创建表单实例
tsxconst methods = useForm({ resolver: zodResolver(schema) }); -
使用 FormProvider 包裹
tsx<FormProvider {...methods}> <form>...</form> </FormProvider> -
子组件使用 useFormContext
tsxconst { register } = useFormContext(); -
动态数组使用 useFieldArray
tsxconst { fields } = useFieldArray({ control, name: 'items' }); -
需要时 useWatch
tsxconst value = useWatch({ control, name: 'field' });
❌ 不应该做的
-
不要传递整个 methods 对象
tsx// ❌ 错误 <CustomerInfo methods={methods} /> // ✅ 正确 <FormProvider {...methods}> <CustomerInfo /> </FormProvider> -
不要在每个组件中都创建表单实例
tsx// ❌ 错误 function CustomerInfo() { const { register } = useForm(); // 每个组件都创建新实例 } -
不要混合使用受控和非受控
tsx// ❌ 错误 const [value, setValue] = useState(''); <input {...register('name')} value={value} />; -
不要过度使用 useWatch
tsx// ❌ 不必要的优化 const name = useWatch({ control, name: 'name' }); // 如果只是显示,直接用 watch 即可 -
不要忽略错误处理
tsx// ❌ 错误 <input {...register('name')} /> // ✅ 正确 <input {...register('name')} /> {errors.name && <p>{errors.name.message}</p>}
总结
核心概念
- 状态提升:在顶层统一管理表单状态
- Context 下沉:通过 FormProvider 和 useFormContext 共享状态
- 按需获取:子组件只获取需要的表单方法
- 按需渲染:使用 useWatch 优化性能
主要优势
- ✅ 无需 Props Drilling
- ✅ 组件独立可复用
- ✅ 性能优化
- ✅ 易于维护
- ✅ 代码简洁
适用场景
- 大型表单
- 多组件协作
- 需要性能优化
- 组件层级较深
- 表单需要复用
最终架构
scss
顶层组件 (OrderForm)
↓ useForm() 创建表单实例
FormProvider {...methods}
↓ 通过 React Context
├─ CustomerInfo (useFormContext)
├─ ShippingInfo (useFormContext)
├─ OrderItems (useFormContext + useFieldArray)
├─ OrderSummary (useFormContext + useWatch)
└─ FormActions (useFormContext)
这种方式让 React Hook Form 的性能优势 和可维护性发挥到了极致!