在React Context中使用react-hook-form出现状态异常的问题咨询
嘿,我看到你把react-hook-form的useForm放在Context Provider里使用时遇到了几个棘手的问题:提交错误不触发、watch看不到输入变化、isDirty这类状态始终停留在初始值,但把useForm直接移到表单组件里就一切正常。我来帮你拆解下原因和解决办法~
问题核心原因
react-hook-form的useForm返回的实例是和组件渲染周期深度绑定的,把它放在Context中时,容易触发以下几个问题:
- Provider渲染导致form实例重置:如果Provider因为props变化(比如children、defaultValues)重新渲染,会重新调用
useForm生成全新的实例,之前的状态直接丢失,组件拿到的是新实例,自然看不到状态变化。 - 状态更新传递不及时:Context的状态更新需要Provider重新渲染才能传递给消费组件,如果Provider没有正确触发渲染,组件就无法及时获取到form的最新状态。
- 自定义组件绑定有误:看你代码里的PhoneInput组件,onChange的处理方式没有正确把完整值传递给react-hook-form的
field.onChange,导致form实例捕获不到输入变化,进而isDirty这类状态不会更新。
具体修复步骤
1. 修正Provider里的defaultValues使用
你的Provider定义了defaultValues参数,但实际用的是未定义的formDefault(应该是笔误),而且没确保初始值的稳定性,这会导致不必要的Provider渲染和form实例重置。修改如下:
// 先定义默认初始值 const defaultFormValues: BuyerForm = { id: undefined, companyName: '', companyType: CompanyTypeEnum.XXX, // 替换为你的默认枚举值 companyPhone: { phone: '', country: {} }, streetLine1: '', streetLine2: '', city: '', stateOrRegion: '', postalCode: '', country: {}, website: '', defaultCurrency: '', contactName: '', contactPosition: '', contactPhone: { phone: '', country: {} }, contactEmail: '', }; export const BuyerFormProvider = ({ children, defaultValues = defaultFormValues, }: { children: ReactNode; defaultValues?: BuyerForm; }) => { // 用useMemo确保defaultValues稳定,避免不必要的useForm重新初始化 const stableDefaultValues = React.useMemo(() => defaultValues, [defaultValues]); const form = useForm<BuyerForm>({ resolver: zodResolver(BuyerFormSchema), mode: 'onSubmit', defaultValues: stableDefaultValues, }); return <BuyerFormContext.Provider value={form}>{children}</BuyerFormContext.Provider>; };
2. 修复自定义组件的onChange绑定
看你PhoneInput的onChange处理,没必要解构后再组装,直接把完整的value对象传给field.onChange即可,这样form实例能正确捕获值的变化;另外别忘了加FormMessage组件,否则提交错误不会显示:
<FormField control={form.control} name='companyPhone' render={({ field }) => ( <FormItem className='flex-1'> <FormLabel>Company Phone</FormLabel> <FormControl> <PhoneInput {...field} onChange={(value) => field.onChange(value)} // 直接传递完整value对象 placeholder='Enter your number' defaultCountry={field.value.country?.alpha2} className='h-10' /> </FormControl> <FormDescription>Include country code (e.g. +44)</FormDescription> <FormMessage /> {/* 必须添加这个组件才会显示错误提示 */} </FormItem> )} />
3. 避免Provider不必要的渲染
可以用React.memo包裹Provider组件,防止无关props变化导致Provider重新渲染:
export const BuyerFormProvider = React.memo(({ children, defaultValues = defaultFormValues, }: { children: ReactNode; defaultValues?: BuyerForm; }) => { // 内部代码不变 });
4. 正确使用watch钩子
watch是react-hook-form的钩子,必须在消费组件内部调用,不能在Context里调用后传递给组件。比如在表单组件里这样用:
const BuyerForm = () => { const form = useBuyerForm(); const { watch } = form; const companyName = watch('companyName'); // 在组件内部调用watch // ... };
为什么放在组件里就正常?
当useForm直接在表单组件里调用时,form实例和组件的渲染周期完全绑定,组件的每一次渲染都会正确反映form的状态变化,而且不会因为外部Provider的渲染而重置实例,所有状态更新都能及时传递到组件内部的表单字段,所以一切正常。
备注:内容来源于stack exchange,提问作者anubhav adarsh

