You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

页面刷新后如何正确同步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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 06:21:05