Русский
Русский
English
Статистика
Реклама

Jsx

React.js формошлепство или работа с формами при помощи пользовательских хуков

06.01.2021 20:05:30 | Автор: admin


В этом посте будет реализован простой пользовательский хук для работы с состояниями формы и будут разобраны способы работы с объектами и массивами. В завершение разберем валидацию форм и реализуем соответствующие функции.


В процессе работы часто приходится решать примерно одинаковые задачи. Для ускорения решения некоторых из них, а также для сокращения кода в целом принято делить код на переиспользуемые фрагменты. В React с версии 16.8.0 появилась возможность использовать хуки. Данная статья затрагивает написание пользовательских хуков. Заранее хочу предупредить: примеры кода и подходы, описанные ниже, не являются оптимальными, их можно и нужно дорабатывать в рамках своих проектов в случае необходимости. Данный код это небольшая демонстрация работы с инпутами.


К слову, в React для удобной работы с формами уже набрали популярность 3 отличных библиотеки. Это Formik, Redux Form и React Hook Form. На сайте последнего представлены плюсы перед конкурентами.


Для начала


Для начала нам нужно создать React приложение. Сделаем это через Create React App. Если информации по ссылке будет недостаточно, то github.


Так как я являюсь поклонником TypeScript, я использовал готовый шаблон для работы с ним используя команду:


npx create-react-app react-custom-forms-article  --template typescript

Наш кастомный хук не должен потребовать серьезных доработок готовых компонентов, особенно если в них не присутствует ни одна из библиотек для работы с формами. Поэтому его вполне можно будет внедрить и в готовый проект.


Приступая к реализации хука


Перед созданием хука надо определиться, как мы будем работать с формой и ее полями. Для удобства можно использовать один объект инициализации формы, в котором изначально будут описываться необходимые поля и их настройки. Объявление этого объекта вынесено за код компонента и выглядит так.


// App.tsxconst formInputs = {  firstName: {},  lastName: '',}

В компоненте инициализируем форму.


// App.tsxconst App = () => {  const { fields, handleSubmit } = useForm(formInputs);  const { firstName } = fields;  return <></>;}

handleSubmit метод, принимающий callback-функцию и возвращающий значение всех полей.
fields структура вида "ключ-значение", в ней будут храниться все описанные ранее поля формы с дополнительной информацией, например: свойство с текстом ошибки, значение поля, флаг валидности поля и те свойства, что будут переданы в объекте при создании формы.


Компонент вернет примерно такую верстку.


// App.tsx// Метод выполнения формы при срабатывание onSubmitconst onSubmit = ({ values }: { values: IValues }) => {  console.log(values, 'submit');}return (  <div className="App">    <form onSubmit={handleSubmit(onSubmit)}>      <input type="text" value={firstName.value} onChange={firstName.setState}/>    </form>  </div>);

useForm


Приступим к реализации хука. Для начала необходимо пройтись по объекту инициализации формы. Во время этого процесса будет происходить проверка значений и объявление необходимых далее свойств.


// hooks/useForm.tsexport const useForm = (initialFields: any = {}) => {  const form = Object.entries(initialFields).reduce((fields, [name, value]: any[]) => {    const isString = typeof value === 'string';    const field = {      [name]: {        value: (isString && value) || ((!isString && value.value) || ''),        setState: (value: ChangeEvent<HTMLInputElement>) => handleInput(value, name),        ...(!isString && value),      }    }    return { ...fields, ...field };  }, {});

В этом примере кода для удобства итераций используется метод объектов entries, который возвращает массив вида [[propName, propValue], [propName, propValue]], и метод для работы с массивами reduce, который помогает собрать объект заново. В целом все выглядит неплохо, но не хватает методов для обновления значения полей. Добавим состояний с использованием React Hook.


// hooks/useForm.ts...const [fields, setState] = useState<any>(form);const handleInput = (element: ChangeEvent<HTMLInputElement>, name: string) => {  const input = fields[name];  const value = element.target.value;  const field = { ...input,  value };  setState(prevState => ({ ...prevState, [name]: field });}

Здесь заводится состояние для полей формы, и в качестве начального значения используется готовая структура формы. Функция handleInput будет необходима для редактирования данных. Как видно из кода, стейт будет обновляться полностью. Это специфика хука useState и текущей реализации. Если бы для работы с состояниями использовалась библиотека RxJs вместо хука useState, то была бы возможность обновлять состояние частично, не провоцируя повторный рендер компонента. В setState в данном примере состояние обновляется также через функцию обратного вызова. При использовании записи вида setState({ ...fields, [name]: field }) изменение другого поля провоцировало бы возврат остальных полей к исходным значениям.


Следующий пример кода проиллюстрирует применение формы.


// hooks/useForm.ts...const handleSubmit = (onSubmit: Function) => (e: FormEvent) => {    if (e && e.preventDefault) {      e.preventDefault();    }    const values = Object.entries(fields).reduce(((prev: any, [name, { value }]: any) => ({ ...prev, [name]: value })), {});    onSubmit({ values });  }

При помощи [каррирования] (https://learn.javascript.ru/currying-partials) принимается переданная из компонента функция и далее при сабмите вызывается с аргументами из хука. Каррирование в примере выше используется для того, чтобы иметь возможность вызвать метод в верстке, не выполняя его при рендере компонента.


Таким образом у нас получился минимальный хук для обычных форм.


Весь код хука
import { ChangeEvent, FormEvent,, useState } from 'react';export const useForm = (initialFields = {}) => {  const form = Object.entries(initialFields).reduce((fields, [name, value]: any[]) => {    const isString = typeof value === 'string';    const field = {      [name]: {        value: (isString && value) || ((!isString && value.value) || ''),        setState: (value: ChangeEvent<HTMLInputElement>) => handleInput(value, name),        ...(!isString && value),      },    };    return { ...fields, ...field };  }, {});  const [fields, setState] = useState<any>(form);  const handleInput = useCallback(    (element: ChangeEvent<HTMLInputElement>, name: string) => {      const input = fields[name];      const value = element.target.value;      const field = { ...input, value };      setState(prevState => ({ ...prevState, [name]: field });    }, [fields, setState]);  const handleSubmit = (onSubmit: Function) => (e: FormEvent) => {    if (e && e.preventDefault) {      e.preventDefault();    }    const values = Object.entries(fields).reduce(((prev: any, [name, {value}]: any) => ({ ...prev, [name]: value })), {});    onSubmit({ values });  }  return {    handleSubmit,    fields,  }}

Типизация


Я не хочу останавливаться на этой теме, так как данный код можно спокойно использовать и без TypeScript, поэтому в коде могут внезапно появляться какие-то типы, чтобы все корректно собрать, в конце статьи будет предоставлена ссылка на github.


Валидация


Теперь, когда наш минимальный хук готов, хочется позаботиться о еще одной проблеме валидации полей. Валидация будет реализована через массив функций.


Итак, немного перепишем схему. Здесь указан массив validators для обоих полей и добавлен флаг обязательности поля.


// App.tsx...const formInputs = {  firstName: {    required: true,    validators: [      (s: string) => !s.length && 'Поле обязательно для заполнения',      (s: string) => s.length < 2 && 'Минимальная длина строки 2',      (s: string) => s.length <= 2 && 'А теперь 3',      (s: string) => parseInt(s) < 2 && 'Должна быть цифра больше 1',    ]  },  datetime: {    validators: [      (s: string) => new Date(s).getUTCFullYear() > new Date().getUTCFullYear() && 'Год рождения не может быть больше текущего',    ],  },}

Помимо обновленной схемы добавим в хук переменную isValid, которая будет запрещать/разрешать отправку формы по кнопке. Рядом с полями также будет выводиться ошибка. К слову, ошибку будем выводить только для тронутых полей.


// App.tsx...const { fields, isValid, handleSubmit } = useForm(formInputs);const { firstName, datetime }: Form = fields;return (    <div className="App">      <form onSubmit={handleSubmit(onSubmit)}>        <input type="text" value={firstName.value} onChange={firstName.setState}/>        <span>{firstName.touched && firstName.error}</span>        <input type="date" value={datetime.value} onChange={datetime.setState}/>        <span>{datetime.touched && datetime.error}</span>        <button disabled={!isValid}>Send form</button>      </form>    </div>);

Разберем этот код. В показанной функции ожидается 2 аргумента, field поле инпута, второй опциональный, в нем будут храниться дополнительные свойства для поля. Далее при помощи деструктуризации объекта заводятся переменные value, required и массив валидаций. Чтобы не менять свойства аргумента, заводятся новые переменные error и valid. Я объявил их через let, так как меняю их в процессе. В коде до обхода массива валидаторов стоит проверка на required, в теле условия проверяется значение поля, и там же прокидывается ошибка.


Мы подошли к условию, где проверяем переменную validators. Она должна быть массивом. Далее по коду создаем массив результатов выполнения функций валидации при помощи метода массива map. validateFn здесь хранится функция, в которую передается значение поля из свойства value. Результат выполнения функции валидации должен быть строкой, так как выводить мы будем именно текст ошибки. Если результат не строка, то возвращаться будет что-то другое. Конкретно здесь пустая строка, но там может быть и другое значение, например false. В любом случае далее происходит фильтрация массива результатов для удаления пустых значений. Таким образом, поле ошибки могло бы быть и массивом ошибок. Но здесь я решил выводить лишь одну ошибку, поэтому далее стоит условие проверки массива result, где присваивается ошибка и меняется состояние valid. В конце выполнения функция fieldValidation возвращает новый объект поля, где записаны все переданные ранее значения + новые, модифицированные.


Далее этот метод будет использоваться в функции handleInput и handleSubmit. В обоих случаях будет прогоняться валидация.


Валидация работает также в связке с общим флагом isValid, который заводится через useState. Сделаем отдельную функцию для обхода полей.


// hooks/useForm.tsconst [isValid, setFormValid] = useState<boolean>(true);const getFormValidationState = (fields) => Object.entries(fields).reduce((isValid: any, [_, value]: any) => Boolean(isValid * value.isValid), true);const handleInput = (element: ChangeEvent<HTMLInputElement>, name: string) => {    const input = fields[name];    const value = element.target.value;    const field = {      ...input,      value,      touched: true,      isValid: true,    };    const validatedField = fieldValidation(field);    setState((prevState) => {      const items = {...prevState, [name]: validatedField};      setFormValid(getFormValidationState(items));      return items;    });  }  const handleSubmit = (onSubmit: Function) => (e: FormEvent) => {    if (e && e.preventDefault) {      e.preventDefault();    }    const fieldsArray = Object.entries(fields);    // Забираем только значения    const values = fieldsArray.reduce(((prev: any, [name, {value}]: any) => ({ ...prev, [name]: value })), {});    // Валидируем каждый инпут    const validatedInputs = fieldsArray.reduce((prev: any, [name, value]: any) => ({ ...prev, [name]: fieldValidation(value, { touched: true }) }), {});    // Изменяем значение стейта isValid    setFormValid(getFormValidationState(validatedInputs));    setState(validatedInputs);    onSubmit({ values });  }

Теперь у нас полностью готов кастомный хук для простых форм с возможностью валидировать значения ввод пользователя. После некоторых манипуляций на выходе получим такой код:


useForm.ts
import { ChangeEvent, FormEvent, useState } from 'react';type IValidatorFN = (s: string) => {};export interface IField {  value?: string;  type?: string;  label?: string;  error?: string;  isValid?: boolean;  required?: boolean;  touched?: boolean;  setState?: (event: ChangeEvent<HTMLInputElement>) => {};  validators?: IValidatorFN[];}export type ICustomField<T = {}> = IField & T;export type ICustomObject<T = {}> = {  [key: string]: ICustomField & T;}export type IValues = {  [key: string]: string | number;}export type IForm = {  fields: ICustomObject;  isValid: boolean;  handleSubmit: (onSubmit: Function) => (e: FormEvent) => void;}type IOptions = {  [key: string]: any;}export const useForm = (initialFields: ICustomObject): IForm => {  const form = Object.entries(initialFields).reduce((fields, [name, value]: any[]) => {    const isString = typeof value === 'string';    const field = {      [name]: {        type: 'text',        value: (isString && value) || ((!isString && value.value) || ''),        error: (!isString && value.error) || null,        validators: (!isString && value.validators) || null,        isValid: (!isString && value.isValid) || true,        required: (!isString && value.required) || false,        touched: false,        setState: (value: ChangeEvent<HTMLInputElement>) => handleInput(value, name),        ...(!isString && value),      },    };    return {...fields, ...field};  }, {});  const [fields, setState] = useState<ICustomObject>(form);  const [isValid, setFormValid] = useState<boolean>(true);  const getFormValidationState = (fields: ICustomObject): boolean =>    Object.entries(fields).reduce((isValid: boolean, [_, value]: any) => Boolean(Number(isValid) * Number(value.isValid)), true);  const fieldValidation = (field: ICustomField, options: IOptions = {}) => {    const { value, required, validators } = field;    let isValid = true, error;    if (required) {      isValid = !!value;      error = isValid ? '' : 'field is required';    }    if (validators && Array.isArray(validators)) {      const results = validators.map(validateFn => {        if (typeof validateFn === 'string') return validateFn;        const validationResult = validateFn(value || '');        return typeof validationResult === 'string' ? validationResult : '';      }).filter(message => message !== '');      if (results.length) {        isValid = false;        error = results[0];      }    }    return { ...field, isValid, error, ...options };  };  const handleInput = (element: ChangeEvent<HTMLInputElement>, name: string) => {    const input = fields[name];    const value = element.target.value;    const field = {      ...input,      value,      touched: true,      isValid: true,    };    const validatedField = fieldValidation(field);    setState((prevState: ICustomObject) => {      const items = {...prevState, [name]: validatedField};      setFormValid(getFormValidationState(items));      return items;    });  }  const handleSubmit = (onSubmit: Function) => (e: FormEvent) => {    if (e && e.preventDefault) {      e.preventDefault();    }    const fieldsArray = Object.entries(fields);    const values = fieldsArray.reduce(((prev: ICustomObject, [name, { value }]: any) => ({ ...prev, [name]: value })), {});    const validatedInputs = fieldsArray.reduce((prev: ICustomObject, [name, value]: any) => ({ ...prev, [name]: fieldValidation(value, { touched: true }) }), {});    setFormValid(getFormValidationState(validatedInputs));    setState(validatedInputs);    onSubmit({ values });  }  return {    fields,    isValid,    handleSubmit,  }}

App.tsx
import React from 'react';import { useForm, IValues } from './hooks/useForm';const formInputs = {  firstName: {    required: true,    validators: [      (s: string) => !s.length && 'Поле обязательно для заполнения',      (s: string) => s.length < 2 && 'Минимальная длина строки 2',      (s: string) => s.length <= 2 && 'А теперь 3',      (s: string) => parseInt(s) < 2 && 'Должна быть цифра, больше 1',    ],    label: 'First Name',  },  datetime: {    type: 'date',    label: 'Birth Date',    validators: [      (s: string) => new Date(s).getUTCFullYear() > new Date().getUTCFullYear() && 'Год рождения не может быть больше текущего',    ],  },  lastName: {    label: 'Last Name',  },}const App = () => {  const { fields, isValid, handleSubmit } = useForm(formInputs);  const { firstName, datetime, lastName } = fields;  const onSubmit = ({ values }: { values: IValues }) => {    console.log(values, 'submit');  }  const formFields = [firstName, lastName, datetime];  return (    <div className="App">      <form onSubmit={handleSubmit(onSubmit)}>        {formFields.map((field, index) => (          <div key={index}>            <input              type={field.type}              placeholder={field.label}              value={field.value}              onChange={field.setState}            />            <span>{field.touched && field.error}</span>          </div>        ))}        <div>          <button disabled={!isValid}>Send form</button>        </div>      </form>    </div>  );}export default App;

Итог


В данной статье мне хотелось привести пример создания собственного хука для работы с формами. Необязательно для работы с ними ограничиваться локальными состояниями или реакт-хуками. Как я упоминал выше, есть возможности и способы оптимизации текущего примера. Основная проблема в этом коде в том, что он вызывает рендер всего компонента при взаимодействии с полями. От этого можно избавиться, если написать реализацию без контроля состояния полей. Однако не всегда неуправляемый вариант подходит для решения той или иной задачи. На помощь может прийти библиотека rxjs или другие, использующие ее возможности, например, такие как focal или rixio. Но это тема для другой статьи.
Спасибо, что дочитали до конца. Я надеюсь, что после ознакомления с этими примерами у вас улучшилось понимание темы кастомных хуков, в частности, работа с формами.
Проект на github

Подробнее..

React Используйте стандартные пропсы для потока данных

11.11.2020 22:08:58 | Автор: admin


Давайте поговорим про поток данных React приложения состоящего из набора форм.

Предполагается, что читатель знаком с react, react-хуками, функциональными компонентами, мемоизацией хорошо знает javascript и не пугается spread операторов (три точки которые).

Я постараюсь подвести вас к логичному для меня выводу, что зачастую компонентам, которые отображают форму или часть формы, достаточно иметь всего три пропса.

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

Например форма редактирования данных о пользователе:

const UserForm = () =>  <FormBlock>    <UserInfo/>    <Experience/>    <Education/>  </FormBlock>

В компоненте UserInfo редактируются поля firstName, lastName.

В компоненте Experience редактируются поля positionName, positionDescription.

В компоненте Education редактируются поля name, description.

Попробуем реализовать компонент UserInfo


Иногда я встречаю такую реализацию:

const UserInfo = ({  firstName,  onChangeFirstName,  lastName,  onChangeLastName,}) =>  <FormBlock>    <Label>First Name</Label>    <Input       value={firstName}      onChange={({ target: { value } }) => onChangeFirstName(value)}    />    <Label>Last Name</Label>    <Input      value={lastName}      onChange={({ target: { value } }) => onChangeLastName(value)}    />  </FormBlock>

И такой вызов из UserForm:

const UserForm = ({  firstName,  onChangeFirstName,  lastName,  onChangeLastName,}) =>  <FormBlock>    <UserInfo      firstName={firstName}      onChangeFirstName={onChangeFirstName}      lastName={lastName}      onChangeLastName={onChangeLastName}    />  </FormBlock>

Но не делайте так. Если пойдет в таком духе, то на входе у UserForm будут все пропсы из компонентов UserInfo, Experience и Education. Мне даже лень этот код вам писать.

Обычно всем тоже лень и в итоге просто вместо прописывания всех пропсов используют spread оператор:

const UserForm = (props) =>  <FormBlock>    <UserInfo {...props} />    <Experience {...props} />    <Education {...props} />  </FormBlock>

И дальше надеются, что каждый компонент сам выберет себе нужные пропсы.

Пожалуйста, так тоже не делайте. Вы подвергаете свой код неявным ошибкам. Мало ли что может залететь в UserForm что не желательно чтобы было в Education. Ну например пропс className или style которые пол года назад использовались чтобы стилизовать UserForm, потом в UserForm это убрали, а в Education такой пропс добавили. И вот кто-то забыл почистить код и где-то остались вызовы UserForm с className. Теперь неожиданно для всех className прокинется в Education.

Всегда явно прокидуйте пропсы чтобы это видно было по коду какие пропсы в какие компоненты попадают.

Что мы можем с этим всем сделать?


Давайте посмотрим на обычные поля ввода которые перекочевали в реакт из HTML. Огромное спасибо разработчикам реакта, что сохранили тот же интерфейс привычный всем, а не, как в ангуляре, напридумывали своих конструкций.

Возьмем, например, тег input. У него есть знакомые всем пропсы: value, onChange и, внимание, name .

По сути это все три пропсы достаточные для передачи потока данных.

Вот так например будет выглядеть теперь UserInfo:

const UserInfo = ({  name,  value,  onChange,}) =>  <FormBlock>    <Label>First Name</Label>    <Input       name={'firstName'}       value={value['firstName']}       onChange={({ target }) => onChange({target: { name, value: { ...value, [target.name]: target.value } }})}    />    <Label>Last Name</Label>    <Input      value={lastName}      onChange={({ target: { value } }) => onChangeLastName(value)}    />  </FormBlock>

Тут я в компоненте UserInfo использую стандартные три пропса. И что немаловажно повторил интерфейс вызова события onChange. Он так же само возвращает информацию о изменениях как это делает стандартный input используя target, name, value. С одной стороны target добавляет еще уровень вложенности, но так уж исторически сложилось у стандартного события onChange, с этим уже ничего не поделаешь. Зато мы получаем очень важное преимущество одинаковое поведение всех полей ввода и частей формы.

То есть мы можем теперь переписать UserForm.

Если у нас данные хранятся как такой объект:

{ firstName, lastName, positionName, positionDescription, name, description }

То пишем так:

const UserForm = ({  name,  value,  onChange,}) =>  <FormBlock>    <UserInfo       value={value}       onChange={({ target }) => onChange({target: { name, value: target.value }})}    />   .......  </FormBlock>

Если у нас данные хранятся как такой объект:

{  userInfo: { firstName, lastName },  position: { positionName, positionDescription },  education: { name, description }}

То пишем так:

const UserForm = ({  name,  value,  onChange,}) =>  <FormBlock>    <UserInfo       name={'userInfo'}       value={value['userInfo']}       onChange={({ target }) => onChange({target: { name, value: { ...value, [target.name]: target.value } }})}    />   .......  </FormBlock>

Как видим, количество пропсов на входе UserForm уменьшилось с 2*N до всего трех.
Но это только часть выгоды.

Как сделать код компактней и читабельней?


Так как у нас теперь везде одинаковый интерфейс, то теперь можно написать вспомогательные функции, которые будут работать со всеми такими компонентами.

Например, представим функцию getInnerProps, которая мапит вложенные данные на вложенные компоненты. Тогда код компонентов становится намного лаконичней:

const UserInfo = ({ name, value, onChange }) => {   const innerProps = getInnerProps({name, value, onChange})   return <FormBlock>    <Label>First Name</Label>    <Input {...innerProps.forInput('firstName')} />    <Label>Last Name</Label>    <Input {...innerProps.forInput('lastName')} />  </FormBlock>}const UserForm = ({  name,  value,  onChange,}) => {  const innerProps = getInnerProps({name, value, onChange})  return <FormBlock>    <UserInfo {...innerProps.forInput('userInfo')} />    <Experience {...innerProps.forInput('position')} />    <Education {...innerProps.forInput('education')} />  </FormBlock>}

Обратите внимание, что одна и та же функция innerProps.forInput() формирует пропсы name, value и onChange и для стандартного поля ввода Input и для компонента UserInfo. Все благодаря одному интерфейсу потока данных.

Усложним пример


Допустим, пользователю нужно ввести несколько образований (education). Один из вариантов решения (на мой взгляд неправильного):

const UserForm = ({  educations,  onChangeEducation,}) =>  <FormBlock>    {Object.entries(educations).map(([id, education]) => <Education      name={name}      description={description}      onChangeName={(name) => onChangeEducation(id, { ...education, name })}      onChangeDescription={(description) => onChangeEducation(id, { ...education, description })}    />}  </FormBlock>

Обработчик onChangeEducation будет менять в нужном месте стора education по его id. Тут есть небольшое противоречие. На вход прилетает коллекция educations, а на событие изменения возвращается один education.

Можно часть кода перенести из Redux в компонент. Тогда станет все логичней. На вход UserForm приходит коллекция educations и на событие изменения уходит тоже коллекция educations:

const UserForm = ({  educations,  onChangeEducations,}) =>  <FormBlock>    {Object.entries(educations).map(([id, education]) => <Education      name={name}      description={description}      onChangeName={(name) => onChangeEducations({ ...educations, [id]: { ...education, name } })}      onChangeDescription={(description) => onChangeEducations({ ...educations, [id]: { ...education, description } })}    />}  </FormBlock>

Немного остановимся на том как мы передаем обработчик в onChangeName и onChangeDescription. Я сознательно не обращал на это внимание для минимизации примеров. Но сейчас это важно.

В реальности компонент Education будет скорее всего мемоизированный (React.memo()). Тогда мемоизация не будет иметь смысла из-за того, что каждый раз мы передаем новую ссылку на функцию. Чтобы не создавать каждый раз новую ссылку используют хук useCallback или useConstant (отдельный npm модуль).

Если в остальных примерах это решало проблему, то тут цикл, а хуки нельзя использовать внутри условий и циклов.

А вот используя name и ожидая от Education стандартного поведения onChange уже можно применить хук useConstant:

const UserForm = ({  name,  value,  onChange,}) => {  const onChangeEducation=({ target }) => useConstant(onChange({    target: {      name,      value: {        ...value,        educations: { ...value.educations, [target.name]: target.value ] }      }    }  }))  return <FormBlock>  {Object.entries(educations).map(([id, education]) => <Education      name={id}      value={education}       onChange={onChangeEducation}    />  )}  </FormBlock>

А теперь сделаем с помощью функции getInnerProps:

const Education = ({ name, value, onChange }) => {   const innerProps = getInnerProps({name, value, onChange})   return <FormBlock>    <Label>Name</Label>    <Input {...innerProps.forInput('name')} />    <Label>Description</Label>    <Input {...innerProps.forInput('description')} />  </FormBlock>}const Educations = ({ name, value, onChange }) => {   const innerProps = getInnerProps({name, value, onChange})   return Object.keys(value).map((id) =>     <Education {...innerProps.forInput('id')} />  )}const UserForm = ({  name,  value,  onChange,}) => {  const innerProps = getInnerProps({name, value, onChange})  return <FormBlock>    <UserInfo {...innerProps.forInput('userInfo')} />    <Experience {...innerProps.forInput('position')} />    <Educations {...innerProps.forInput('educations')} />  </FormBlock>}

Вроде как достаточно красивый и понятный код получился.

Несколько слов про стейт


Подключим stateless компонент UserInfo к стейту и замкнем поток данных. В качестве примера возьмем Redux.

Вот так иногда реализовывают редюсер:

const reducer = (state = initState, action) {  switch(action.type) {    case CHANGE_FIRST_NAME:       return { ...state, userInfo: { ...state.userInfo, firstName: action.payload } }    case CHANGE_LAST_NAME:       return { ...state, userInfo: { ...state.userInfo, lastName: action.payload } }   ........  }}

В этом подходе я вижу два сомнительных плюса и один большой минус.

Первый плюс это то, что можно написать тест для этого редсера. Сомнителен потому что вряд ли этот тест сильно поможет.

Второй плюс то что можно отдельно подконектить чуть ли не каждый инпут к отдельному полю в сторе и будет обновлятся только это связанное поле ввода. Тут еще не факт что это даст увеличение производительности. Проитерироваться по 10 мемоизированным частям формы, в результате чего будет перерисована только одна часть это практически не скажется на производительности.

Минус в том что придется писать очень много кода: для каждого поля изменение стейта, потом добавить action, прокинуть значение, вызвать на каждое событие отдельный action.

Да, в документации по редаксу говорят, что надо писать редюсеры не такие у которых только set, а такие в которых побольше екшенов. Типа чем больше экшенов в реюсере, тем больше тестов можно написать. Больше тестов меньше ошибок.

Но я думаю меньше ошибок там, где меньше кода, а много экшенав нужно писать только там где это надо.

Для себя я решил что для форм в редаксе, везде где это возможно, использую только один action какой-нибудь SET.

const reducer = (state = initState, action) {  switch(action.type) {    case SET_USER_FORM_DATA:       return { ...state, value: action.payload }     ........  }}

А уже на UI (т.е. в реакте) я определяю какие поля в какой части данных меняются.

const UserFormContainer = () => {  const dispatch = useDispatch()  return <UserForm    value={useSelector(({ userForm }) => userForm?.value)}    onChange={({target: { value } }) => dispatch(userFormActions.set(value)}  />}

Потому как логику специфических полей не опишешь в редаксе. Например поле ввода номера телефона это может быть сложный реакт компонент компонент, а не просто изменение значение в стейте.

Когда применять описанный подход


Имейте ввиду. Это не подход на все случаи жизни. Все описанное применимо в основном к приложениям в которых много форм, где одна форма собирается из набора других форм и поток данных направлен из стора в форму контейнер, из нее в составные части формы, из них еще на уровень ниже.

Если у вас приложение со сложным интерфесом в котором разные компоненты взаимодействуют друг с другом описанное в статье мало что вам даст. Как раз в этом случае логично каждый компонент подключать к стору.

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

Резюме


Давайте использовать одинаковые пропсы для потока данных, пропсы которые уже давно есть в HTML:

  • name
  • value,
  • onChange({target: { name, value }})

Старайтесь в onChange придерживаться той же структуры, как и в реактовском onChange.

Старайтесь на onChange в target.value возвращать ту же сущность что на вход в value.

Тогда, за счет использования стандартного подхода и общих хелперных функций для этого подхода, код станет лаконичней и понятней.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru