Повторное использование форм на React

от автора

Привет!

У нас в БКС есть админка и множество форм, но в React-сообществе нет общепринятого метода — как их проектировать для переиспользования. В официальном гайде Facebook’a нет подробной информации о том, как работать с формами в реальных условиях, где нужна валидация и переиспользование. Кто-то использует redux-form, formik, final-form или вообще пишет свое решение.

В этой статье мы покажем один из вариантов работы с формами на React. Наш стек будет вот таким: React + formik + Typescript. Мы покажем:

  • Что компонент должен делать.
  • Конфиг, поля и валидация на уровне пропсов.
  • Как сделать форму переиспользуемой.
  • Оптимизацию перерендера.
  • Чем наш способ неудобен.

При новой бизнес-задаче мы узнали, что нам нужно будет сделать 15-20 похожих форм, и гипотетически их может стать еще больше. У нас была одна форма-динозавр на конфиге, которая работала с данными из `store`, отправляла actions на сохранение и выполнение запросов через `sagas`. Она была замечательной, выполняла бизнес-велью. Но уже была нерасширяемой и непереиспользуемой, только при плохом коде и добавлении костылей.

Задача поставлена: переписать форму для того, чтобы ее можно было переиспользовать неограниченное количество раз. Хорошо, вспоминаем функциональное программирование, в нем есть чистые функции, которые не используют внешние данные, в нашем случае `redux`, только то, что им присылают в аргументах (пропсах).

И вот что получилось.

Идея нашего компонента заключается в том, что ты создаешь обертку (контейнер) и пишешь в ней логику работы с внешним миром (получение данных из стора Redux и отправка экшенов). Для этого компонент-контейнер должен иметь возможность получать какую-то информацию через колбеки. Весь список пропсов формы:

interface IFormProps {   // сообщает форме когда ей показывать лоадер и дизейблить кнопки   IsSubmitting?: boolean;   // текс для кнопки отправки   submitText?: string;   //текст для кнопки отмены   resetText?: string;   // стоит ли валидировать при изменении поля (пропс для формика)   validateOnChange?: boolean;    // стоит ли валидировать при blur’e поля (пропс для формика)   validateOnBlur?: boolean;   // конфиг, на основе которого будут рендериться поля.   config: IFieldsFormMetaModel[];   // значения полей.   fields: FormFields;    // схема для валидации   validationSchema: Yup.MidexSchema;   // колбек при сабмите формы   onSubmit?: () => void;   // колбек при клике на reset кнопку   onReset?: (e: React.MouseEvent<HTMLElement>) => void;   // изменение конкретного поля   onChangeField?: (     e: React.SyntaticEvent<HTMLInputElement, name: string; value: string   ) => void;    // присылает все поля на изменение + валидны ли они   onChangeFields?: (values: FormFields, prop: { isValid }) => void;  } 

Использование Formik

Мы используем компонент <Formik />.

render() {   const {     fields, validationSchema, validateOnBlur = true, validateOnChange = true,   } = this.props;    return (     <Formik       initialValues={fields}       render={this.renderForm}       onSubmit={this.handleSubmitForm}       validationSchema={validationSchema}       validateOnBlur={validateOnBlur}       validateOnChange={validateOnChange}       validate={this.validateFormLevel}     />   ); } 

В prop’e формика `validate` мы вызываем метод `this.validateFormLevel`, в котором компоненту-контейнеру даем возможность получить все измененные поля и проверить, валидны ли они.

private validateFormLevel = (values: FormFields) => {   const { onChangeFields, validationSchema } = this.props;    if (onChangeFields) {     validationSchema       .validate(values)       .then(() => {         onChangeFields(values, { isValid: true });        })       .catch(() => {          onChangeFields(values, { isValid: false });        });    } } 

Здесь приходится вызывать еще раз валидацию для того, чтобы дать понять контейнеру, валидны ли поля. При сабмите формы мы просто вызываем prop `onSubmit`:

private handleSubmitForm = (): void => {   const { onSubmit } = this.props;    if (onSubmit) {     onSubmit();   } } 

С пропсами 1-5 все должно быть понятно. Перейдем к ‘config’, ‘fields’ и ‘validationSchema’.

Пропс ‘config’

interface IFieldsFormMetaModel {   /** Имя секции */   sectionName?: string;   sectionDescription?: string;   fieldsForm?: Array<{     /** Название поля формы */     name?: string; // по значению этого поля будет будет находить ключ из prop ‘fields’     /** Является ли поле checked */     checked?: boolean;     /** enum, возможные варианты для отображения поля */     type?: ElementTypes;     /** Текст для лейбла */     label?: string;     /** Текст под полем */     helperText?: string;     /** Признак обязательности заполнения элемента формы */     required?: boolean;     /** Признак доступности поля для изменения */     disabled?: boolean;     /** Минимальное кол-во элементов в поле */     minLength?: number;     /** Объект с начальным значением куда входит само значение и его описание */     initialValue?: IInitialValue;     /** Массив значений для выпадающих списков */     selectItems?: ISelectItems[]; // значения для select, dropdown и подобных   }>; }

На основе этого интерфейса создаем массив объектов и рендерим по такой схеме “раздел” -> “поля раздела”. Так мы можем показывать несколько полей для раздела или в каждом по одному, если нужен заголовок и примечание. Как устроен рендер, покажем немного позже.
Короткий пример конфига:

export const config: IFieldsFormMetaModel[] = [   {     sectionName: 'Общая информация',     fieldsForm: [{       name: 'subject',       label: 'Тема',       type: ElementTypes.Text,     }],   },   {     sectionName: 'Напоминание',     sectionDescription: 'Напоминание для сотрудника',     fieldsForm: [{       name: 'reminder',       disabled: true,       label: 'Сотруднику',       type: ElementTypes.CheckBox,       checked: true,     }],   }, ];

На основе бизнес-данных задаются значения для ключей `name`. Эти же значения используются в ключах prop `fields` для передачи первоначальных или измененных значений для формика.

Для примера выше `fields` может выглядеть так:

const fields: SomeBusinessApiFields = {   subject: 'Встреча с клиентом',   reminder: 'yes', }

Для валидации нам нужно передавать Yup Schema. Форме мы отдаем схему с пропсами контейнера, описывая там взаимодействия с внешними данными, например, запросами.

Форма никак не может повлиять на схему, пример:

export const CreateClientSchema: (   props: CreateClientProps, ) => Yup.MixedSchema =   (props: CreateClientProps) => Yup.object(     {       subject: Yup.string(),       description: Yup.string(),       date: dateSchema,       address: addressSchema(props),     },   );

Рендер и оптимизация полей

Для рендера мы сделали мапу, для быстрого поиска по ключу. Выглядит лаконично и поиск быстрее, чем по `switch`.

fieldsMap: Record<   ElementTypes,   (     state: FormikFieldState,     handlers: FormikHandlersState,     field: IFieldsFormInfo,   ) => JSX.Element   > = {     [ElementTypes.Text]: (       state: FormikFieldState,       handlers: FormikHandlersState,       field: IFieldsFormInfo     ) => {       const { values, errors, touched } = state;        return (         <FormTextField           key={field.name}           element={field}           handleChange={this.handleChangeField(handlers.setFieldValue, field.name)}           handleBlur={handlers.handleBlur}           value={values[field.name]}           error={touched[field.name] && errors[field.name] || ''}         />       );     },     [ElementTypes.TextSearch]: (...) => {...},     [ElementTypes.TextArea]: (...) => {...},     [ElementTypes.Date]: (...) => {...},     [ElementTypes.CheckBox]: (...) => {...},     [ElementTypes.RadioButton]: (...) => {...},     [ElementTypes.Select]: (...) => {...},   };

Каждый компонент-поле является stateful. Он находится в отдельном файле и обернут в `React.memo`. Все значения передаются через props, минуя `children`, чтобы избежать лишнего перерендера.

Заключение

Наша форма неидеальна, для каждого кейса нам приходится создавать контейнер обертку для работы с данными. Сохранять их в `store`, конвертировать и делать запросы. Присутствует повторение кода, от которого хочется избавиться. Мы пробуем найти новое решение, при котором форма в зависимости от пропсов будет брать нужный ключ из стора с полями, экшены, схемы и конфиг. В одном из следующих постов мы расскажем, что из этого получилось.


ссылка на оригинал статьи https://habr.com/ru/company/bcs_company/blog/461919/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *