Реагирование-перехват -формирует неожиданное поведение с многоуровневым вложенным объектом

#reactjs #typescript #react-hooks #react-hook-form

#reactjs #typescript #реагирование-перехваты #react-hook-form

Вопрос:

Недавно я обнаружил плагин react-hook-from, который на первый взгляд кажется идеальным для начала использования и замены других плагинов из-за его высокой производительности.

После использования плагина для некоторых действительно простых форм я наткнулся на сложную форму, которую я хочу обработать с помощью плагина. Моя форма основана на вложенном объекте, который имеет следующую структуру. (Определение Typescript)

 type model = {
   tag: string;
   visible: boolean;
   columns?: (modelColumn | model)[];
}

type modelColumn = {
   property: string;
}
  

Итак, для обработки n-уровневой вложенной формы я создал следующие компоненты.

 const initialData = {
    ...
    datasource: {
        tag: "tag1",
        visible: true,
        columns: [
            {
                property: "property",
            },
            {
                property: "property1",
            }, 
            {
                tag: "tag2",
                visible: true,
                columns: [
                    {
                        property: "property",
                    }, 
                    {
                        tag: "tag3",
                        visible: false,
                        columns: [
                            {
                                property: "property",
                            }
                        ]
                    }
                ]
            },
            {
                entity: "tag4",
                visible: false,
            }
        ],
    },
    ...
}

export const EditorContent: React.FunctionComponent<EditorContentProps> = (props: any) => {
   const form = useForm({
        mode: 'all',
        defaultValues: initialData,
    });

    return (
        <FormProvider {...form}>
                <form>
                    <Handler 
                        path="datasource"
                    />
                </form>
        </FormProvider>
    );
}
  

В вышеупомянутом компоненте создается основная форма, загруженная исходными данными. (Я привел примерное значение). Handler Компонент имеет логин рекурсии со свойством path для вызова вложенной логики типа данных form. Вот пример реализации.

 ...
import { get, isNil } from "lodash";
import { useFieldArray, useForm, useFormContext, useWatch } from "react-hook-form";
...

export type HandlerProps = {
    path: string;
    index?: number;
    ...
}

export const Handler: React.FunctionComponent<HandlerProps> = (props) => {
    const { path, index, onDelete, ...rest } = props;

    const { control } = useFormContext();

    const name = isNil(index)? `${path}` :`${path}[${index}]`;
  
    const { fields, append, insert, remove } = useFieldArray({
        control: control,
        name: `${name}.columns`
    });

    ...

    const value = useWatch({
        control,
        name: `${name}`,
    });

    ...
    const addHandler = () => {
        append({ property: null });
    };

    console.log(`Render path ${name}`);

    return (
        <React.Fragment>
            <Row>
                <Col>
                    <FormField name={`${name}.tag`} defaultValue={value.tag}>
                        <Input
                            label={`Tag`}
                        />
                    </FormField>
                </Col>
                <Col>
                     <FormField defaultValue={value.visible} name={`${name}.visible`} >
                         <Switch />
                     </FormField>
                </Col>
                <Col>
                    <button onClick={addHandler}>Add Column Property</button>
                </Col>
            </Row>
            {
                fields amp;amp; (
                    fields.map((field: any, _index: number) => {
                        if (field.property !== undefined) {
                            return (
                                <Column 
                                    path={`${name}.columns`}
                                    index={_index}
                                    control={control}
                                    fields={fields}
                                    onDelete={() => remove(_index) }
                                />
                            )
                        } else {
                            return (
                                <Handler 
                                    path={`${name}.columns`}
                                    index={_index}
                                />
                            )
                        }
                    })
                )
            }
        </React.Fragment>
    );
}
  

По сути, компонент-обработчик использует контекст формы и вызывает сам себя, если он должен отображать вложенный объект формы, такой как datasource.columns[x], который регистрируется в useFieldArray для получения его столбцов. Пока все работает нормально. Я правильно отображаю полное дерево (если можно так выразиться), как форму объекта.

Для справки здесь приведен код компонента Column, а также для вспомогательного компонента FormField FormField .

 export const Column: React.FunctionComponent<ColumnProps> = (props) => {
    const { fields, control, path, index, onDelete } = props;

    const value = useWatch({
        control,
        name: `${path}[${index}]`,
        defaultValue: !isNil(fields[index])? fields[index]: { 
            property: null,
        }
    });

    console.log(`Render of Column ${path} ${value.property}`);
    
    return (
        <Row>
            <Col>
                <button onClick={onDelete}>Remove Property</button>
            </Col>
            <Col>
                <FormField name={`${path}[${index}].property`} defaultValue={value.property}>
                    <Input
                        label={`Column Property name`}
                    />
                </FormField>
            </Col>
        </Row>
    );
}


export type FormFieldProps = {
    name: string;
    disabled?: boolean;
    validation?: any;
    defaultValue?: any;
    children?: any;
};

export const FormField: React.FunctionComponent<FormFieldProps> = (props) => {
    const { 
        children, 
        name, 
        validation,
        defaultValue,
        disabled,
    } = props;

    const { errors, control, setValue  } = useFormContext();

    return (
        <Controller
            name={name}
            control={control}
            defaultValue={defaultValue? defaultValue: null}
            rules={{ required: true }}
            render={props => {
                return (
                    React.cloneElement(children, { 
                        ...children.props, 
                        ...{
                            disabled: disabled || children.props.disabled,
                            value: props.value,
                            onChange: (v:any) => {
                                props.onChange(v);
                                setValue(name, v, { shouldValidate: true });
                            },
                        } 
                    })
                );
            }}
        />
    );
}


  

Проблема заключается в том, когда я удаляю поле из массива. Компонент обработчика повторно отображает fields данные с правильными значениями. Но значения useWatch содержат исходные данные, что приводит к неправильному отображению с отображением неправильных полей, и формы начинают объединяться.

Есть ли какие-либо советы о том, как правильно отобразить такую вложенную форму. Я предполагаю, что это не проблема с формой react-hook, но в реализации есть ошибка, которая, по-видимому, вызывает проблему.