页面刷新后如何正确同步Zustand与React Hook Form数据?
问题分析
你遇到的核心问题是React Hook Form(RHF)内部状态与Zustand持久化状态不同步:
- Input组件中直接用Zustand的
field.value覆盖了RHF的fieldProps.value,导致RHF内部未记录用户输入/持久化数据 - 页面刷新后,Zustand从本地存储恢复数据,Input显示正常,但RHF内部状态仍为初始空值,提交时Zod验证自然失败
- 临时的
useEffect + reset虽能解决问题,但多表单场景下重复代码过多,不是最优解
解决方案
方案1:让RHF作为表单状态唯一数据源,同步更新Zustand
这是最简洁的实现方式,避免双向绑定的状态混乱。修改Input组件,不再手动指定value,让RHF控制输入值,同时在onChange时同步到Zustand:
// InputFields.tsx export default function InputFields() { const { control } = useFormContext(); const { updateForm } = useFormStore(useShallow((state) => ({ updateForm: state.updateForm, }))); const formFields: Field[] = [ { name: "name", label: "Name", placeholder: "Enter name" }, { name: "address", label: "Address", placeholder: "Enter address" }, { name: "email", label: "Email", placeholder: "Enter email" }, ]; return ( <> {formFields.map((field) => ( <FormField key={field.name} control={control} name={field.name} render={({ field: fieldProps }) => { return ( <FormItem> <FormLabel>{field.label}</FormLabel> <FormControl> <Input placeholder={field.placeholder} {...fieldProps} // 移除手动指定的value,交给RHF控制 onChange={(e) => { fieldProps.onChange(e); // 同步输入值到Zustand updateForm({ [field.name]: e.target.value }); }} /> </FormControl> <FormMessage /> </FormItem> ); }} /> ))} </> ); }
随后修改Page组件,直接从Zustand获取持久化数据作为RHF的默认值,无需额外useEffect:
// Page.tsx export default function Page() { const savedForm = useFormStore((state) => state.form); const form = useForm<z.infer<typeof FormSchema>>({ resolver: zodResolver(FormSchema), // 直接用Zustand的持久化数据作为RHF初始值 defaultValues: savedForm, }); const { formState: { isSubmitting }, } = form; const onSubmit = (data: z.infer<typeof FormSchema>) => { console.log(data); }; return ( <FormProvider {...form}> <form onSubmit={form.handleSubmit(onSubmit)}> <InputFields /> <Button type="submit" disabled={isSubmitting}> {isSubmitting ? "Submitting..." : "Submit"} </Button> </form> </FormProvider> ); }
页面刷新时,RHF会直接加载Zustand的持久化数据作为初始状态,内部状态与输入显示完全同步,提交验证自然正常。
方案2:封装自定义Hook复用同步逻辑(多表单场景)
如果有多个表单需要与Zustand同步,可以封装自定义Hook抽离同步逻辑,避免重复代码:
// hooks/useFormWithZustand.ts import { useForm, UseFormProps } from "react-hook-form"; import { useFormStore } from "@/stores/form-store"; import { useEffect } from "react"; export function useFormWithZustand<T extends object>(props: Omit<UseFormProps<T>, "defaultValues">) { const { form, updateForm } = useFormStore(); const formInstance = useForm<T>({ ...props, defaultValues: form as T, }); // 监听Zustand状态变化,同步到RHF(可选,适用于其他地方修改Zustand的场景) useEffect(() => { formInstance.reset(form as T); }, [form, formInstance.reset]); // 监听RHF表单变化,自动同步到Zustand useEffect(() => { const unsubscribe = formInstance.watch((data) => { updateForm(data); }); return unsubscribe; }, [formInstance, updateForm]); return formInstance; }
在Page组件中直接使用这个自定义Hook:
// Page.tsx export default function Page() { const form = useFormWithZustand<z.infer<typeof FormSchema>>({ resolver: zodResolver(FormSchema), }); const { formState: { isSubmitting }, } = form; const onSubmit = (data: z.infer<typeof FormSchema>) => { console.log(data); }; return ( <FormProvider {...form}> <form onSubmit={form.handleSubmit(onSubmit)}> <InputFields /> <Button type="submit" disabled={isSubmitting}> {isSubmitting ? "Submitting..." : "Submit"} </Button> </form> </FormProvider> ); }
同时Input组件可以进一步简化,无需手动处理onChange同步(自定义Hook已通过watch自动同步):
// InputFields.tsx export default function InputFields() { const { control } = useFormContext(); const formFields: Field[] = [ { name: "name", label: "Name", placeholder: "Enter name" }, { name: "address", label: "Address", placeholder: "Enter address" }, { name: "email", label: "Email", placeholder: "Enter email" }, ]; return ( <> {formFields.map((field) => ( <FormField key={field.name} control={control} name={field.name} render={({ field: fieldProps }) => { return ( <FormItem> <FormLabel>{field.label}</FormLabel> <FormControl> <Input placeholder={field.placeholder} {...fieldProps} /> </FormControl> <FormMessage /> </FormItem> ); }} /> ))} </> ); }
这个方案适合多表单场景,一次封装即可在所有表单中复用,彻底解决重复代码问题。
关键注意点
- 避免同时让RHF和Zustand控制Input的
value,会导致状态冲突 - 优先让RHF作为表单状态的唯一入口,Zustand仅做持久化备份
- 若表单字段较多,可在
watch中指定监听字段优化性能:formInstance.watch(["name", "email"], (data) => updateForm(data))
内容的提问来源于stack exchange,提问作者Julien Heng
相关产品推荐
相关产品推荐

