React Hook Form - defaultValues not changing
React Hook Form defaultValues does not reset automatically after first rendering
Example of Full Component:
import { IconBadge } from "@/components/icon-badge";import { LayoutDashboard, ArrowLeft } from "lucide-react";import { CareerForm } from "./_components/career-form";import Link from "next/link";import { useEffect, useState } from "react";import Loader from "@/components/loader";import { Actions } from "./_components/actions";import { getCareer } from "@/core/query";import { Career } from "@/core/types";const CareerIdPage = ({ params }: { params: { companyId: string; careerId: string } }) => {const { companyId, careerId } = params;const [refresh, setRefresh] = useState<boolean>(false);const { data: career, error } = getCareer(companyId, careerId);// Handling loading or error statesif (!career && !error) {return <Loader />;}if (error) {return <div>Error loading career data: {error.message}</div>;}// Define required fields for career setup completionconst requiredFields = [career.title]; // Expand this as neededconst totalFields = requiredFields.length;const completedFields = requiredFields.filter(Boolean).length;const completionText = `(${completedFields}/${totalFields})`;const isComplete = requiredFields.every(Boolean);return (<div className="p-6"><Link href={`/admin/companies/${companyId}/careers`} className="flex items-center text-sm hover:opacity-75 transition mb-6"><ArrowLeft className="h-4 w-4 mr-2" /> Back to careers</Link><div className="flex items-center justify-between"><div className="flex flex-col gap-y-2"><h1 className="text-2xl font-medium">Career Setup</h1><span className="text-sm text-slate-700">Complete all fields {completionText}</span></div><div><Actions career={career} setRefresh={setRefresh} /></div></div><div className="w-full"><div className="flex items-center gap-x-2"><IconBadge icon={LayoutDashboard} /><h2 className="text-xl">Customize your career</h2></div><CareerForm initialData={career} setRefresh={setRefresh} /></div></div>);};export default CareerIdPage;
"use client";import * as z from "zod";import axios from "axios";import { zodResolver } from "@hookform/resolvers/zod";import { useForm } from "react-hook-form";import toast from "react-hot-toast";import { Dispatch, SetStateAction, Fragment } from "react";// Importing form section componentsimport { InputForm } from "./input-form";import { BenefitsForm } from "./benefits-form";import { RequirementsForm } from "./requirements-form";import { ResponsibilitiesForm } from "./responsibilities-form";import { SkillsForm } from "./skills-form";// Importing types and constantsimport { Career } from "@/types";import { form_attrs, formSchema } from "./form-type";interface CareerFormProps {initialData: Career;setRefresh: Dispatch<SetStateAction<boolean>>;}export const CareerForm = ({ initialData, setRefresh }: CareerFormProps) => {// Initialize useForm hook with validation and default valuesconst form = useForm<z.infer<typeof formSchema>>({resolver: zodResolver(formSchema),defaultValues: initialData,});const { isSubmitting, isValid, handleSubmit, errors } = form.formState;// onSubmit function to handle form submissionconst onSubmit = async (values: z.infer<typeof formSchema>) => {try {// Sending PATCH request to update career dataawait axios.patch(`${process.env.NEXT_PUBLIC_API_URL}/api/companies/${initialData.companyId}/careers/${initialData.id}`,values,{withCredentials: true,headers: { "Content-Type": "application/json" },});// Success message and trigger refreshtoast.success("Career updated");setRefresh((prev) => !prev);} catch (error: any) {// Error handlingtoast.error("Something went wrong");}};// Function to render the corresponding form section based on the attributeconst renderFormSection = (form_attr: string) => {const formProps = { form }; // Assuming all form sections need access to the `form` objectswitch (form_attr) {case "responsibilities":return <ResponsibilitiesForm {...formProps} />;case "benefits":return <BenefitsForm {...formProps} />;case "requirements":return <RequirementsForm {...formProps} />;case "skills":return <SkillsForm {...formProps} />;default:return <InputForm {...formProps} />;}};return (<div className="flex flex-row flex-wrap gap-x-5 gap-y-5"><form onSubmit={handleSubmit(onSubmit)}>{/* Dynamically render form sections */}{form_attrs.map((form_attr) => (<Fragment key={form_attr}>{renderFormSection(form_attr)}{/* Display error messages if validation fails */}{errors[form_attr] && (<p className="text-red-500 text-sm">{errors[form_attr]?.message}</p>)}</Fragment>))}{/* Submit button */}<button type="submit" disabled={isSubmitting || !isValid}>{isSubmitting ? "Submitting..." : "Submit"}</button></form></div>);};
but the problem is when the careerid is changed and new data is fetched but the defaultValue of useForm is not changing
Ah, I see! You're encountering an issue where the defaultValue
of the useForm
hook in react-hook-form
isn't updating when the careerId
changes and new data is fetched. This is a common issue because useForm
only accepts its defaultValues
once, during the initial render, and doesn't automatically update when the data changes after the initial mount.
To fix this, you need to explicitly reset the form when the new career data arrives. React Hook Form provides a reset
method, which can be used to update the form's values with new data.
Here’s how you can handle this:
reset
to Update Form Data Dynamically
Solution: Use -
Use
useEffect
to Watch forinitialData
Changes: WheneverinitialData
(your fetched career data) changes, you can call thereset
method to update the form with the new data. -
Update
useForm
with the New Data: You’ll need to callform.reset(initialData)
inside auseEffect
that listens for changes ininitialData
(your fetched data). This ensures the form's values are reset whenever new data is fetched, even if thecareerId
changes.
reset
Logic:
Updated Code with "use client";import * as z from "zod";import axios from "axios";import { zodResolver } from "@hookform/resolvers/zod";import { useForm } from "react-hook-form";import { Dispatch, SetStateAction, Fragment, useEffect } from "react";import toast from "react-hot-toast";import { Career } from "@/types";import { form_attrs, formSchema } from "./form-type";import { BenefitsForm } from "./benefits-form";import { RequirementsForm } from "./requirements-form";import { ResponsibilitiesForm } from "./responsibilities-form";import { SkillsForm } from "./skills-form";interface CareerFormProps {initialData: Career;setRefresh: Dispatch<SetStateAction<boolean>>;}export const CareerForm = ({ initialData, setRefresh }: CareerFormProps) => {const form = useForm<z.infer<typeof formSchema>>({resolver: zodResolver(formSchema),defaultValues: initialData,});// This is to be addeduseEffect(() => {form.reset(initialData); // Reset form when initialData changes}, [initialData, form]);//const { isSubmitting, isValid, handleSubmit, formState: { errors } } = form;const onSubmit = async (values: z.infer<typeof formSchema>) => {try {await axios.patch(`${process.env.NEXT_PUBLIC_API_URL}/api/companies/${initialData.companyId}/careers/${initialData.id}`,values,{withCredentials: true,headers: { "Content-Type": "application/json" },});toast.success("Career updated");setRefresh((prev) => !prev);} catch (error: any) {console.error(error);toast.error(error.response?.data?.message || "Something went wrong");}};const renderFormSection = (form_attr: string) => {const formProps = { ...form }; // Assuming formProps contains the necessary data for all form sectionsswitch (form_attr) {case "responsibilities":return <ResponsibilitiesForm {...formProps} />;case "benefits":return <BenefitsForm {...formProps} />;case "requirements":return <RequirementsForm {...formProps} />;case "skills":return <SkillsForm {...formProps} />;default:return <InputForm {...formProps} />;}};return (<div className="flex flex-row flex-wrap gap-x-5 gap-y-5"><form onSubmit={handleSubmit(onSubmit)}>{form_attrs.map((form_attr) => (<Fragment key={form_attr}>{renderFormSection(form_attr)}{errors[form_attr] && (<p className="text-red-500 text-sm">{errors[form_attr]?.message}</p>)}</Fragment>))}<button type="submit" disabled={isSubmitting || !isValid}>{isSubmitting ? "Submitting..." : "Submit"}</button></form></div>);};
Explanation of Changes:
useEffect
withform.reset
:useEffect
is watching theinitialData
prop for changes.- When
initialData
changes (for example, when thecareerId
changes and new data is fetched), the form is reset withform.reset(initialData)
to update the form fields with the new data.
- Calling
form.reset
:- This updates the form with new values when the career data changes, even after the initial mount. This ensures the form correctly reflects the latest data when a new career is selected.
Key Points:
form.reset(initialData)
: Resets the form to the newinitialData
, ensuring that the form reflects the updated values.useEffect
Dependency: The effect runs every time theinitialData
changes, which triggers the form reset with the new data.
Important Notes:
- Make sure that
initialData
is notnull
orundefined
when you callform.reset
. You may want to add a check or a loading state ifinitialData
is initially empty before fetching. - If you're updating data frequently, be careful with the performance implications of calling
form.reset
repeatedly. However, for most use cases, this is fine and ensures that the form stays in sync with the state.
With this approach, the form will automatically update with the new initialData
whenever it changes, solving the issue where the form doesn't reflect updated data when the careerId
changes.