Это будет и всегда был Уилл Смит, который руководит двумя ненавистниками if / else спагетти-кода в их личном аду зависимостей в формах реакции.

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

Прежде чем мы начнем: наша задача (также известная как боль)

Последние три месяца 2018 года мы с Flo Sloot работали над пошаговой формой, написанной на React. Конечно, основная задача этой формы заключалась в том, чтобы собрать как можно больше данных о клиентах, обеспечивая при этом наилучшее взаимодействие с пользователем. К сожалению, это довольно сложно, потому что чем больше полей формы должен заполнить клиент, тем меньше он настроен нажимать «далее» и заполнять еще несколько.
В результате такого поведения мы решили скрыть многие поля по умолчанию. По мере того, как пользователи проходят нашу форму, некоторые поля могут появляться в зависимости от введенных пользователем данных.

Давайте быстро резюмируем требования:

  • Более одного шага (волшебное поведение)
  • Зависимость между значениями полей (если поле A равно X → показать поле B)
  • Зависимость между значением поля и полным набором полей (если поле A равно X → показать набор полей B, который содержит поля c, d, e…)
  • Зависимости также могут быть на разных шагах (если поле A равно X на шаге 1 → показать поле B на шаге 5)

База проекта была в значительной степени определена с самого начала, поэтому мы опирались на это:

  • Реагировать
  • MobX как государственный менеджер

И последнее, о чем стоит упомянуть, - это государственное устройство. Короче говоря: он был вложенным, и мы не могли абстрагировать правила для зависимостей, что в основном означает, что любой атрибут на первом уровне нашего объекта состояния может зависеть от значения глубоко вложенного атрибута (или / и наоборот )

Спагетти рай

Наше первое решение было довольно простым. Мы решили попробовать вычисленные свойства (MobX).
В итоге у нас в магазине скопилось множество вычислений. На самом деле это сработало, но способ реализации новой зависимости доставил нам головную боль.
Мы всегда начинали с реализации новой функции `@ computed` вроде этой:

@computed get showFieldB() { 
 return state.nested.fieldA === ‘someValue’ 
}

Это закончилось в каком-то JSX вот так:

const { showFieldB } = this.props
return (  
  <>    
    { showFieldB && <MyConditionalComp /> }  
  </>
)

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

«Большая воля» спешит на помощь

Вы можете догадаться, что нас не удовлетворило приведенное выше решение. Вот почему однажды вечером мы сели и пришли к идее.

Нам нужен микросервис, который предоставляет помощников для вывода логических значений в зависимости от заданной схемы.

Управление зависимостями, день независимости, Уилл Смит - поехали. Мы позвонили во внутреннюю службу «Смит».

Что мы хотим? Наша цель состояла в том, чтобы позволить одному объекту соответствовать всем нашим зависимостям. Этот объект должен иметь ту же структуру, что и наше состояние.

Шаг 1:
Мы написали несколько вспомогательных функций для итерации свойств объекта по заданному пути. Например, hello.my.name будет искать name в заданном вложенном объекте. Вы можете найти функцию для этого здесь.

Шаг 2:
Поскольку мы смогли перебрать путь, мы решили проверить, установлен ли атрибут condition для каждого из элементов пути. Если это так, мы вызовем условие, которое должно быть функцией, с набором аргументов:

  • объект состояния, содержащий все значения объекта (в нашем поле case)
  • значение фактически пройденного атрибута пути
  • набор вспомогательных функций для лучшей читаемости и часто используемых функций

Шаг 3:
После написания нескольких тестов мы решили также добавить один компонент React в качестве оболочки. Эта оболочка должна делать не что иное, как определять, отображается ли обернутое содержимое на основе заданного пути, состояния и объекта условия. В итоге мы получили что-то вроде этого:

<DependencyWrapper 
  state={myState} 
  path="myState.person.firstname" 
  rules={conditionTree} >
    <p>Here we go with some content</p>
</DependencyWrapper/>

Шаг 4:
То, как мы используем DependencyWrapper Component в нашем основном приложении, сильно зависит от нашего состояния.
Когда мы используем Mobx, мы внедрили наш FormState и сделали класс наблюдаемым ( Скорее всего, можно было бы подключить компонент).
Опять почти псевдокод того, что мы придумали:

@inject("ourStore")
@observer
class DependantField extends React.Component {
  render() {
    const state = this.props.ourStore
    const conditions = someImportedConditionObject
    const path = this.props.path
  
    return <DependencyWrapper 
      state={state} 
      conditions={conditions} 
      path={path}
    />
  }
}

Использование этого компонента извне выглядело довольно чисто:

<DependantField path="my.nested.path.value" />

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

{
  person: {
    personal: {
      firstname: "",
      lastname: ""
    },
    address: {
      city: "",
      street: "",
      zip: ""
   }
}

Таким образом, мы хотим отображать все поля адреса только в том случае, если имя и фамилия заполнены. В результате получается объект условия, подобный этому:

{
  address: {
    condition: (state, fieldValue, helper) => 
      state.person.personal.firstname.length &&
      state.person.personal.lastname.length
  }
}

Конечно, вы также можете использовать вспомогательную функцию. На своем пути к свойствам объекта наш «волевой» сервис в конечном итоге обнаружит условие на address и остановится на нем, вычислив значение данной функции.

Заключение

Созданная нами служба предоставляет некоторые вычисления на основе заданного состояния, пути и объекта условия, предлагает несколько помощников и компонент, который мы можем использовать повторно. Мы используем некоторые функции Mobx, чтобы использовать эту службу в нашем приложении.
Все заканчивается чистыми render() методами и лучшей читабельностью. Более того, мы можем описать наши (иногда довольно тяжелые) условия более сфокусированными и более «изолированными». Наконец, мы можем писать тесты для нашего полного объекта условия, что в конечном итоге требует меньше shallow рендеринга в нашем jest testsuite.

В конце концов, каждый подход имеет свои издержки, поэтому стоит упомянуть, что мы должны соблюдать это в будущем. Поскольку мы разделяем путь и ищем свойства объекта каждого разделенного / конкатенированного элемента на каждом рендере, это может привести к некоторым проблемам с производительностью (в зависимости от количества полей, которые вы рендерит, и уровней гнездование в вашем состоянии / состоянии).

Я хотел бы поделиться еще одним кодом, но он был разработан внутри компании. Если вы хотите узнать больше или просто немного поболтать, свяжитесь со мной и / или Flo Sloot 🗣

Хороших выходных!