reactjsfrontendreact-hook-formreact-querytanstackreact-query

React hook form + react query. Sync defaultValues on refetch without clobbering dirty fields, and hard-reset after submit


Context

I’m using React Hook Form together with React Query. The parent component fetches data with a query, derives defaultValues, and passes them into the form.

Parent component:

export const Agreement = ({ clientId }: { clientId: number }) => {
  const { data } = useGetServiceAgreement();

  const defaultValues = {
    description: data.description,
    clientCommissionPercent: data.clientCommissionPercent,
  };

  return <AgreementForm defaultValues={defaultValues} />
};

Form component:

const FORM_RESET_CONFIG = {
  keepDirtyValues: true,
  keepErrors: true,
  keepTouched: true,
  keepIsSubmitted: true,
  keepSubmitCount: true,
  keepIsValid: true,
} as const;

export const AgreementForm = ({
  defaultValues,
}: {
  defaultValues: ClientAgreementFormData;
}) => {
  const methods = useForm<ClientAgreementFormData>({
    resolver: yupResolver(ClientAgreementSchema),
    defaultValues: defaultValues ?? ClientAgreementSchema.getDefault(),
  });
  const { reset, handleSubmit } = methods;

  useEffect(() => {
    reset(defaultValues, FORM_RESET_CONFIG);
  }, [defaultValues, reset]);

  const onSubmit = async (data: ClientAgreementFormData) => {};

  return form

What I need

React Query may refetch on window focus (refetchOnWindowFocus: true). In that case, I want to update the form with the new server values only for fields the user hasn’t touched. I’m currently doing:

reset(defaultValues, FORM_RESET_CONFIG); // with keepDirtyValues: true, etc.

After a successful submit, I want to fully reset the form to the values returned by the server (clearing dirty/touched state):

reset(valuesFromPostResponse); // without FORM_RESET_CONFIG

Questions

Is it a good idea to sync form values with updated defaultValues when the query refetches? Any recommended best practices or pitfalls?

What’s the recommended pattern to support both flows in a single form:

Partial sync on refetch (preserving dirty fields), and full reset after a successful submit?


Solution

  • Update your code like this:

    // Parent Component
    import { AgreementForm } from "./AgreementForm";
    import { useGetServiceAgreement } from "./hooks/useGetServiceAgreement";
    
    export const Agreement = ({ clientId }: { clientId: number }) => {
      const { data, isLoading, isError } = useGetServiceAgreement(clientId);
    
      if (isLoading) return <div>Loading...</div>;
      if (isError) return <div>Failed to load agreement data.</div>;
    
      return <AgreementForm initialData={data} />;
    };
    
    //Form Component
    
    import React from "react";
    import { useForm, FormProvider } from "react-hook-form";
    import { yupResolver } from "@hookform/resolvers/yup";
    import { ClientAgreementSchema, ClientAgreementFormData } from "./schema";
    import { useUpdateServiceAgreement } from "./hooks/useUpdateServiceAgreement";
    
    type AgreementFormProps = {
      initialData: ClientAgreementFormData;
    };
    
    export const AgreementForm: React.FC<AgreementFormProps> = ({ initialData }) => {
      const mutation = useUpdateServiceAgreement();
    
      // Initialize the form with `values` to keep it reactive
      const methods = useForm<ClientAgreementFormData>({
        values: initialData ?? ClientAgreementSchema.getDefault(),
        resolver: yupResolver(ClientAgreementSchema),
        resetOptions: { keepDirtyValues: true }, // Preserve user-edited fields on refetch
      });
    
      const { handleSubmit, reset, register, formState } = methods;
    
      const onSubmit = async (formData: ClientAgreementFormData) => {
        try {
          const updated = await mutation.mutateAsync(formData);
          // Full reset after successful submission
          reset(updated);
          alert("Agreement saved successfully!");
        } catch (error) {
          console.error("Failed to save:", error);
          alert("Failed to save agreement.");
        }
      };
    
      return (
        <FormProvider {...methods}>
          <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4">
            <div>
              <label>Description</label>
              <input
                {...register("description")}
                className="border rounded p-2 w-full"
              />
              {formState.errors.description && (
                <span className="text-red-600">
                  {formState.errors.description.message}
                </span>
              )}
            </div>
    
            <div>
              <label>Client Commission (%)</label>
              <input
                type="number"
                {...register("clientCommissionPercent")}
                className="border rounded p-2 w-full"
              />
              {formState.errors.clientCommissionPercent && (
                <span className="text-red-600">
                  {formState.errors.clientCommissionPercent.message}
                </span>
              )}
            </div>
    
            <button
              type="submit"
              className="bg-blue-600 text-white rounded px-4 py-2 hover:bg-blue-700"
            >
              Save
            </button>
          </form>
        </FormProvider>
      );
    };
    
    //Schema Example
    import * as yup from "yup";
    
    export const ClientAgreementSchema = yup.object({
      description: yup.string().required("Description is required"),
      clientCommissionPercent: yup
        .number()
        .min(0, "Must be at least 0")
        .max(100, "Must be at most 100")
        .required("Client commission is required"),
    });
    
    export type ClientAgreementFormData = yup.InferType<typeof ClientAgreementSchema>;
    
    ClientAgreementSchema.getDefault = (): ClientAgreementFormData => ({
      description: "",
      clientCommissionPercent: 0,
    });
    
    /** Why This Pattern Works
    
    values keeps the form reactive to data changes without manual useEffect resets.
    
    resetOptions.keepDirtyValues protects user edits from being overwritten by query refetches.
    
    Manual reset after submission lets you clear dirty/touched state and reflect server-confirmed values.
    */