4

I have made a small codesandbox with reproducible bug here. I have a form component where I am setting useForm hook with initial values. Form has an IncomeInfo and a component with array field as child components:

const { pageFormValues, setPageFormValues, setActiveStep } = usePageProvider();
const initialValues = pageFormValues ?? createInitialValues(income);
const [action, setAction] = useState<ActionStatus>(ActionStatus.IDLE);

const useFormMethods = useForm({
        defaultValues: initialValues,
});


return (
    <FormProvider {...useFormMethods}>
        <form onSubmit={useFormMethods.handleSubmit(onSubmit)}>
           <IncomeInfo />
           <IncomeBaseTable initialValues={initialValues} />

IncomeBaseTable looks like this:

export const IncomeBaseTable = ({ initialValues }) => (
    <TableWrapper heading={["Period", "Employer", "Total", ""]}>
        <ArrayField
            name="incomeBase"
            children={({ index, handleRemove, handleAppend }) => (
                <TableRowWrapper
                    cells={[
                        <div className="flex gap-x-4">
                            <FormControlledDatePicker
                                key={`incomeBase[${index}].from`}
                                name={`incomeBase[${index}].from`}
                                label="From"
                                placeholder="DD.MM.ÅÅÅÅ"
                                defaultValue={initialValues.incomeBase[index].from}
                                hideLabel
                            />
                            <FormControlledDatePicker
                                key={`incomeBase[${index}].to`}
                                name={`incomeBase[${index}].to`}
                                label="To"
                                placeholder="DD.MM.ÅÅÅÅ"
                                defaultValue={initialValues.incomeBase[index].to}
                                hideLabel
                            />
                        </div>,
                        <FormControlledTextField
                            key={`incomeBase[${index}].employer`}
                            name={`incomeBase[${index}].employer`}
                            label="Employer"
                            hideLabel
                        />,
                        <FormControlledTextField
                            key={`incomeBase[${index}].total`}
                            name={`incomeBase[${index}].total`}
                            label="Total"
                            type="number"
                            hideLabel
                        />,
                        <Button
                            key={`delete-button-${index}`}
                            onClick={() => handleRemove(index)}
                            icon={<Delete aria-hidden />}
                            variant="tertiary"
                            size="xsmall"
                        />,
                    ]}
                />
            )}
        />
    </TableWrapper>
);

TableWrapper looks like this:

export const TableWrapper = ({ heading, children }: { heading: string[]; children: ReactNode }) => (
    <Table size="small">
        <Table.Header>
            <Table.Row>
                {heading.map((header) => (
                    <Table.HeaderCell scope="col" key={header}>
                        {header}
                    </Table.HeaderCell>
                ))}
            </Table.Row>
        </Table.Header>
        <Table.Body>{children}</Table.Body>
    </Table>
);

export const TableRowWrapper = ({ cells }: { cells: ReactElement[] }) => (
    <Table.Row>
        {cells.map((cell, index) => {
            if (!index)
                return (
                    <Table.HeaderCell scope="row" key={cell.key}>
                        {cell}
                    </Table.HeaderCell>
                );
            return <Table.DataCell key={index}>{cell}</Table.DataCell>;
        })}
    </Table.Row>
);

When I am appending from IncomeInfo to the field array like this, values get updated, but fields are the same as they were before the append action:

const { control, getValues } = useFormContext();
const { append, remove } = useFieldArray({
    control,
    name: "incomeBase",
});

const handleOnChange = (checked, incomeValues) => {
    if (checked) {
        append({
            from: null,
            to: null,
            employer: "",
            total: incomeValues.sum,
        });
    }
};

When I am logging in the ArrayFields component I can see that I get new values with getValues method, but fields are not updated:

const ArrayFields = ({ name, children }: ArrayFieldsProps) => {
    const { control, getValues } = useFormContext();
    const { fields, append, remove } = useFieldArray({
        control,
        name: name,
    });

    const handleAppend = (value) => {
        append(value);
        console.log("add: ", getValues());
    };

    const handleRemove = (index) => {
        remove(index);
        console.log("remove: ", getValues());
    };

    console.log("field", fields);
    console.log("value", getValues().incomeBase);

Since fields are not updated the new field is not rendered. Why are values updated, but not fields?

If I append to the array field from IncomeBaseTable component, then it appends and rerenders with new fields:

export const IncomeBaseTable = ({ initialValues }) => (
    <TableWrapper heading={["Period", "Employer", "Total", ""]}>
        <ArrayField
            name="incomeBase"
            children={({ index, handleRemove, handleAppend }) => (
                <TableRowWrapper
                    cells={[
                        <div className="flex gap-x-4">
                            <FormControlledDatePicker
                                key={`incomeBase[${index}].from`}
                                name={`incomeBase[${index}].from`}
                                label="From"
                                placeholder="DD.MM.ÅÅÅÅ"
                                defaultValue={initialValues.incomeBase[index].from}
                                hideLabel
                            />
                            <FormControlledDatePicker
                                key={`incomeBase[${index}].to`}
                                name={`incomeBase[${index}].to`}
                                label="To"
                                placeholder="DD.MM.ÅÅÅÅ"
                                defaultValue={initialValues.incomeBase[index].to}
                                hideLabel
                            />
                        </div>,
                        <FormControlledTextField
                            key={`incomeBase[${index}].employer`}
                            name={`incomeBase[${index}].employer`}
                            label="Employer"
                            hideLabel
                        />,
                        <FormControlledTextField
                            key={`incomeBase[${index}].total`}
                            name={`incomeBase[${index}].total`}
                            label="Total"
                            type="number"
                            hideLabel
                        />,
                        <Button
                            key={`add-button-${index}`}
                            onClick={() => handleAppend(fieldInitialValues)}
                            icon={<Add aria-hidden />}
                            variant="tertiary"
                            size="xsmall"
                        />,
                    ]}
                />
            )}
        />
    </TableWrapper>
);

Why is it adding to field array and rerendering when I append from the IncomeBaseTable, but not when I append from IncomeInfo component?

UPDATE

I managed to get it to work by using one instance of the useArrayField in the parent component and then passing it as a prop to child components. It won't work when I have more instances with same field name. Not sure why is that, beats the purpose of being a hook if I need to pass it around as a prop.

1
  • Can you explain in simple words what is the issue you are facing? I tried reading it but still don't quite understand and what should happen, the sandbox I used has a error in it and there is a code difference between here and the sandbox. Commented Mar 2, 2023 at 13:03

2 Answers 2

1

react-hook-form uses proxies under the hood to reduce the number of renders. In your case, you update fields outside of the hook useArrayField. This hook have his own way to detect changes and are not expecting external changes.

You could forward useArrayField values to <IncomeInfo/> or use useWatch() to get the fields values.

EDIT: There is a good example in the last section of the doc (Controlled Field Array) : https://react-hook-form.com/api/usefieldarray/

Sign up to request clarification or add additional context in comments.

7 Comments

ArrayField is my component it has nothing to do with hook-form, in both places component I useArrayField. I have updated my question with a codesandbox where you can reproduce the bug.
my bad for the confusion. I updated my answer but I think it still apply as you're updating fields collection externally.
You can check out codesandox that I added, you can see there.
Looks like I need to use the same instance of useArrayField, it won't work when I have more instances with same name. Not sure why is that, beats the purpose of being a hook if I need to pass around it as a prop.
Yeah, I realized that, I feel it is a bit annoying that I can't call hook from several places in the code, but I need to pass it as a prop.
|
0

I would suggest getting the fields or updated fields from useFormContext itself so that you can access the form context or updated fields.

Here is IncomeInfo where you will append fields

import { useFieldArray, useFormContext } from "react-hook-form";
import { v1 as uuid} from "uuid";
export const IncomeInfo = ({ fieldArrayMethods }) => {
  const { getValues,control } = useFormContext();
  const { append, remove } = useFieldArray({
    control,
    name: "incomeBase"
  });

  const handleOnClick = () => {
    append({
      from: "",
      to: "",
      employer: "",
      total: "",
      id: uuid()
    });
  };
  console.log("append: ", getValues());

  return (
    <button type="button" onClick={() => handleOnClick()}>
      add from outside
    </button>
  );
};

Get the fields in IncomeBaseTable with the help of getValues() overloaded menthod , specifically getValues(name?: name).

import { TableWrapper, TableRowWrapper } from "./TableWrapper";
import { Button } from "@navikt/ds-react";
import ArrayFields from "./ArrayField";
import { FormControlledTextField } from "./FormControlledTextField";
import { useFormContext } from "react-hook-form";

export const IncomeBaseTable = ({ initialValues, fieldArrayMethods }) => {
  const {getValues} = useFormContext();
  const incomeBaseFields = getValues("incomeBase");

  console.log("updated/initial fields",incomeBaseFields)
  return(
    <TableWrapper heading={["From", "To", "Employer", "Total", ""]}>
      <ArrayFields
        name="incomeBase"
        fields={incomeBaseFields}
        children={({ index }) => (
          <TableRowWrapper
            cells={[
              <FormControlledTextField
                key={`incomeBase[${index}].from`}
                name={`incomeBase[${index}].from`}
              />,
              <FormControlledTextField
                key={`incomeBase[${index}].to`}
                name={`incomeBase[${index}].to`}
              />,
              <FormControlledTextField
                key={`incomeBase[${index}].employer`}
                name={`incomeBase[${index}].employer`}
              />,
              <FormControlledTextField
                key={`incomeBase[${index}].total`}
                name={`incomeBase[${index}].total`}
              />,
              <Button
                key={`add-button-${index}`}
                type="button"
                onClick={() =>
                  fieldArrayMethods.append({
                    from: null,
                    to: null,
                    employer: "",
                    total: 123213
                  })
                }
                variant="tertiary"
                size="xsmall"
              >
                Add from inside
              </Button>
            ]}
          />
        )}
      />
    </TableWrapper>
  );
}

Check the fix here

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.