-1

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?

2 Answers 2

1

In that case, I want to update the form with the new server values only for fields the user hasn’t touched.

You can achieve that by using the values API from react-hook-form instead of defaultValues:

https://react-hook-form.com/docs/useform#values

values stay reactive, so they will update as new data comes int. Here’s an example from the FAQs:

function App() {
  const { data } = useQuery() // data returns { firstName: '', lastName: '' }
  const { register, handleSubmit } = useForm({
    values: data,
    resetOptions: {
      keepDirtyValues: true, // keep dirty fields unchanged, but update defaultValues
    },
  })
}
Sign up to request clarification or add additional context in comments.

4 Comments

I need keepDirtyValues if values changed but reset whole form after submit. How can I achieve that? I wonder do we even have to do that? Is that real case in production?
I think you pass keepDirtValues: true to useForm and keepDirtyValues: false to reset that you call after a form submit
What @TkDodo said sound correct to me.
OP, I am interested in your original usecase for this. You keep asking if it's a 'real case in production', that somewhat depends on what you want to do?
0

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.
*/


2 Comments

Thanks for answer. 1 question here: "Do we really have to do that in real-world production project?"
Yes, this is a practical, production-grade pattern when: You use React Query or any data-fetching that might refetch; You care about preserving form edits; You want to keep server-sync clean and intentional.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.