Maintain state of useFieldArray react-hook-form

Viewed 4577

I built a multistep form using react-hook-form with a dynamic fields array using useFieldArray.

Documentation: useFieldArray documentation

Here is the full working code link: React Multi-step form with useFieldArray

In the 2nd step when I add new fields using add a dog button, everything works fine, the new data of step is saved to localstorage using little state machine.

enter image description here

But when I click the previous button, the added fields disappear whereas data is still in localstorage.

code for 2nd step:

import { useForm, useFieldArray } from "react-hook-form";
import { useStateMachine } from "little-state-machine";
import updateAction from "./updateAction";
import { useNavigate } from "react-router-dom";

function Step2(props) {
const {
    register,
    control,
    handleSubmit,
    watch,
    formState: { errors },
} = useForm({
    defaultValues: {
        test: [{ nameOfDog: "Bill", ageOfDog: "2", sizeOfDog: "small" }],
    },
});

const { fields, append, remove } = useFieldArray({
    control,
    shouldUnregister: true,
    name: "test",
});

const elements = watch("test");
console.log(elements, fields);

const { actions, state } = useStateMachine({ updateAction });
const navigate = useNavigate();
const onSubmit = (data) => {
    // console.log(fields);
    actions.updateAction(data);
    navigate("/step3");
};

let dta;
if (state.date2) {
    dta = new Date(state.date2);
} else {
    dta = new Date();
    dta.setDate(dta.getDate() + 1);
}

return (
    <form className="form" onSubmit={handleSubmit(onSubmit)}>
        <div className="stepn stepn-active" data-step="1">
            {fields.map((item, index) => {
                return (
                    <div className="row" key={item.id}>
                        <div className="col">
                            <label htmlFor="nameOfDog">Name:</label>
                            <input
                                id="nameOfDog"
                                {...register(`test.${index}.nameOfDog`, {
                                    required: true,
                                })}
                                defaultValue={item.nameOfDog}
                            />
                            {errors.nameOfDog && (
                                <span>This field is required</span>
                            )}
                        </div>
                        <div className="col">
                            <label htmlFor="ageOfDog">Age:</label>
                            <input
                                id="ageOfDog"
                                type="number"
                                {...register(`test.${index}.ageOfDog`, {
                                    required: true,
                                })}
                                defaultValue={item.ageOfDog}
                            />
                            {errors.ageOfDog && (
                                <span>This field is required</span>
                            )}
                        </div>
                        <div className="col">
                            <label htmlFor="sizeOfDog">Size in Lbs:</label>
                            <select
                                id="sizeOfDog"
                                {...register(`test.${index}.sizeOfDog`, {
                                    required: true,
                                })}
                                defaultValue={item.sizeOfDog || ""}
                            >
                                <option value="small">Small (40)</option>
                                <option value="large">Large (40+)</option>
                            </select>
                            {errors.sizeOfDog && (
                                <span>Please Select an option</span>
                            )}
                        </div>
                        <div className="col">
                            <button
                                onClick={(e) => {
                                    e.preventDefault();
                                    remove(index);
                                }}
                                style={{ padding: "26px 62px" }}
                            >
                                Delete
                            </button>
                        </div>
                    </div>
                );
            })}
            <div className="row">
                <div className="col">
                    <button
                        onClick={(e) => {
                            e.preventDefault();
                            append({
                                nameOfDog: "Bill2",
                                ageOfDog: "5",
                                sizeOfDog: "large",
                            });
                        }}
                    >
                        Add a Dog
                    </button>
                </div>
            </div>
        </div>

        {/* <input type="submit" /> */}
        <div className="row">
            <button className="prev" onClick={() => navigate("/")}>
                Previous
            </button>
            <button className="next">Next</button>
        </div>
    </form>
    );
   }

 export default Step2;

{fields.map((item, index) =>

whenever the previous button is clicked, fields array resets to default.

enter image description here

All the remaining steps of the form except 2nd step is being saved when we go back to previous step.

How do i keep the fields in the 2nd step saved when I click the previous button.

2 Answers

There are two problems here:

  • you don't update your state in "Step 2" when you click on the "Previous" button. So you have to pass the current form data to your state machine. Additionally you also have no form validation for "Step 2" right now, when you want to go a previous step. To add support for validation you should move handleSubmit from the <form /> element and instead pass it to your two <button /> elements. This way you can get rid of the watch call as you have the current form data inside the handleSubmit callback.
const onPrevious = (data) => {
  actions.updateAction(data);

  navigate("/");
};
const onNext = (data) => {
  actions.updateAction(data);

  navigate("/step3");
};
<div className="row">
  <button className="prev" onClick={handleSubmit(onPrevious)}>
    Previous
  </button>
  <button className="next" onClick={handleSubmit(onNext)}>
    Next
  </button>
</div>

If you want to keep handleSubmit in the <form /> element, you should use watch and pass the data to your state machine before you navigate back to the previous step.

const test = watch("test");

const onPrevious = (data) => {
  actions.updateAction({ test });

  navigate("/");
};
  • as you reinitialise each step component on a step change you have to pass the current defaultValues to useForm for each step. For "Step 2" it would look like this:
const {
  register,
  control,
  handleSubmit,
  watch,
  formState: { errors }
} = useForm({
  defaultValues: {
    test: state.test ?? [
      { nameOfDog: "Bill", ageOfDog: "2", sizeOfDog: "small" }
    ]
  }
});

The important thing to change is, that when you pass the defaultValues for your fields within the useForm config, you should remove it from the <Controller /> components. I made it for "Step 1", so you have an example there.

Edit determined-moon-j8b7b

It's very long but maybe we can figure it out.

The use is correct, the problem in my opinion is that you're not checking the state and just printing the default values everytime

Related