Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React hook form - appending to array field updates values, but not fields

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.

like image 353
Leff Avatar asked Feb 04 '26 22:02

Leff


1 Answers

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/

like image 127
Kevin Gilbert Avatar answered Feb 06 '26 12:02

Kevin Gilbert



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!