Разработка формы на React. Принципы KISS, YAGNI, DRY на практике

Здавствуйте, в этом туториале мы рассмотрим как разработать очень простую, но контролируемую форму в React, сфокусировавшись на качестве кода.

При разработке нашей формы мы будем следовать принципам «KISS», «YAGNI», «DRY». Для успешного прохождения данного туториала вам не нужно знать этих принципов, я буду объяснять их по ходу дела. Однако, я полагаю, что вы хорошо владеете современным javascript и умеете мыслить на React.



Структура туториала:







За дело! Пишем простую форму, используя KISS и YAGNI


Итак, представим, что у нас есть задание реализовать форму авторизации:
Код для копирования
const logInData = {
  nickname: 'Vasya',
  email: 'pupkin@gmail.com',
  password: 'Reac5$$$',
};



Начнем нашу разработку с анализа принципов KISS и YAGNI, временно забывая об остальных принципах.

KISS — «Оставьте код простым и тупым». Думаю, с понятием простого кода вы знакомы. Но что значит «тупой» код? В моем понимании, это код, который решает задачу, используя минимальное количество абстракций, при этом вложенность этих абстракций друг в друга также минимальна.

YAGNI — «Вам это не понадобится». Код должен уметь делать только то, для чего он написан. Мы не создаем никакой функционал, который может понадобиться потом или который делает приложение лучше на наш взгляд. Делаем только то, что нужно конкретно для реализации поставленной задачи.

Давайте будем строго следовать этим принципам, но также учтем:

  • initialData и onSubmit для LogInForm приходит с верху (это полезный прием, особенно когда форма должна уметь обрабатывать create и update одновременно)
  • должна иметь для каждого поля label

К тому же, давайте упустим стилизацию, так как она нам не интересна, и валидацю, поскольку это тема для отдельного туториала.

Пожалуйста, реализуйте форму самостоятельно, следуя принципам, описанным выше.

Моя реализация формы
Код для копирования
const LogInForm = ({ initialData, onSubmit }) => {
  const [logInData, setLogInData] = useState(initialData);

  const handleSubmit = e => {
    e.preventDefault();
    onSubmit(logInData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Enter your nickname
        <input
          value={logInData.nickname}
          onChange={e => setLogInData({ ...logInData, nickname: e.target.value })}
        />
      </label>
      <label>
        Enter your email
        <input
          type="email"
          value={logInData.email}
          onChange={e => setLogInData({ ...logInData, email: e.target.value })}
        />
      </label>
      <label>
        Enter your password
        <input
          type="password"
          value={logInData.password}
          onChange={e => setLogInData({ ...logInData, password: e.target.value })}
        />
      </label>
      <button>Submit</button>
    </form>
  );
};
    






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

Но если вы сразу создали одну функцию-обработчик для всех полей, то это уже не является «тупейшим» решением задачи. А наша цель на этом этапе — создать максимально простой и «тупой» код, чтобы потом посмотреть на всю картинку и выделить самые лучше абстракции.

Если у вас получился точно такой же код, это круто и означает, что наше с вами мышление сходится!

Далее мы будем работотать с этим кодом. Он прост, но пока далек от идеала.




Рефакторинг и DRY


Пришло время разобраться с принципом DRY.

DRY, упрощенная формулировка — «не дублируйте свой код». Принцип кажется простым, но в нем есть подвох: для избавления от дублирования кода нужно создавать абстракции. Если эти абстракции будут недостаточно хороши, мы нарушим принцип KISS.

Также важно понимать, что DRY нужен не для того, чтобы писать код быстрее. Его задача — упростить чтение и поддержку нашего решения. Поэтому не спешите создавать абстракции сразу. Лучше сделать простую реализацию какой-то части кода, а потом проанализировать, какие абстракции нужно создать, чтобы упростить чтение кода и уменьшить количество мест для изменений, когда они понадобятся.

Чеклист правильной абстракции:
  • имя абстракции полностью соответствует ее назначению
  • абстракция выполняет конкретную, понятную задачу
  • чтение кода, из которого была выделена абстракция, улучшилось

Итак, давайте приступим к рефакторингу.
А у нас есть ярко выраженный дублирующийся код:
Код для копирования
  <label>
    Enter your email
    <input
      type="email"
      value={logInData.email}
      onChange={e => setLogInData({ ...logInData, email: e.target.value })}
    />
  </label>
  <label>
    Enter your password
    <input
      type="password"
      value={logInData.password}
      onChange={e => setLogInData({ ...logInData, password: e.target.value })}
    />
  </label>



В данном коде дублируется композиция из 2-х элементов: label, input. Давайте объеденим их в новую абстракцию InputField:
Код для копирования
  <label>
    Enter your email
    <input
      type="email"
      value={logInData.email}
      onChange={e => setLogInData({ ...logInData, email: e.target.value })}
    />
  </label>
  <label>
    Enter your password
    <input
      type="password"
      value={logInData.password}
      onChange={e => setLogInData({ ...logInData, password: e.target.value })}
    />
  </label>



Теперь наш LogInForm выглядит так:
Код для копирования
const LogInForm = ({ initialData, onSubmit }) => {
  const [logInData, setLogInData] = useState(initialData);

  const handleSubmit = e => {
    e.preventDefault();
    onSubmit(logInData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <InputField
        label="Enter your nickname"
        value={logInData.nickname}
        onChange={e => setLogInData({ ...logInData, nickname: e.target.value })}
      />
      <InputField
        type="email"
        label="Enter your email"
        value={logInData.email}
        onChange={e => setLogInData({ ...logInData, email: e.target.value })}
      />
      <InputField
        type="password"
        label="Enter your password"
        value={logInData.password}
        onChange={e => setLogInData({ ...logInData, password: e.target.value })}
      />
      <button>Submit</button>
    </form>
  );
};



Читать стало проще. Имя абстракции соответсвует задаче, которую она решает. Цель компонента очевидна. Кода стало меньше. Значит мы идем в правильном направлении!

Сейчас видно, что в InputField.onChange дублируется логика.
То, что там происходит, можно разбить на 2 этапа:

Код для копирования
const stage1 = e => e.target.value;
const stage2 = password => setLogInData({ ...logInData, password });



Первая функция описывает детали получения значения с события input. У нас есть на выбор 2 абстракции, в которых мы можем хранить эту логику: InputField и LogInForm.

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

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

Если вас смущает, что вам в будущем может понадобиться событие, вспомните о принципе YAGNI. Всегда можно будет добавить дополнительный prop onChangeEvent в наш компонент InputField.

А до тех пор InputField будет выглядеть так:
Код для копирования
const InputField = ({ label, type, value, onChange }) => (
  <label>
    {label}
    <input
      type={type}
      value={value}
      onChange={e => onChange(e.target.value)}
    />
  </label>
);



Таким образом соблюдается однородность типа при вводе и выводе в компонент и скрывается истинная природа происходящего для внешнего кода. Если нам в будущем понадобится другой компонент ui, например, checkbox или select, то в нем мы тоже будем сохранять однородность типа на вводе-выводе.

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

Этот эвристический прием встроен по-умолчанию во многие фреймворки. Например, это основная идея v-model во Vue, который многие любят за его простоту работы с формами.

Вернемся к делу, обновим наш компонент LogInForm в соответсвии с изменениями в InputField:
Код для копирования
const LogInForm = ({ initialData, onSubmit }) => {
  const [logInData, setLogInData] = useState(initialData);

  const handleSubmit = e => {
    e.preventDefault();
    onSubmit(logInData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <InputField
        label="Enter your nickname"
        value={logInData.nickname}
        onChange={nickname => setLogInData({ ...logInData, nickname })}
      />
      <InputField
        type="email"
        label="Enter your email"
        value={logInData.email}
        onChange={email => setLogInData({ ...logInData, email })}
      />
      <InputField
        type="password"
        label="Enter your password"
        value={logInData.password}
        onChange={password => setLogInData({ ...logInData, password })}
      />
      <button>Submit</button>
    </form>
  );
};



Это выглядит уже совсем неплохо, но мы можем еще лучше!

Callback, который передается в onChange, всегда делает одно и то же. В нем меняется только ключ: password, email, nickname. Значит, мы можем заменить его на такой вызов функции: handleChange('password').

Давайте реализуем эту функцию:
Код для копирования
  const handleChange = fieldName => fieldValue => {
    setLogInData({
      ...logInData,
      [fieldName]: fieldValue,
    });
  };



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

Посмотрим на получившийся код:
Код для копирования
  const LogInForm = ({ initialData, onSubmit }) => {
    const [logInData, setLogInData] = useState(initialData);
  
    const handleSubmit = e => {
      e.preventDefault();
      onSubmit(logInData);
    };
  
    const handleChange = fieldName => fieldValue => {
      setLogInData({
        ...logInData,
        [fieldName]: fieldValue,
      });
    };
  
    return (
      <form onSubmit={handleSubmit}>
        <InputField
          label="Enter your nickname"
          value={logInData.nickname}
          onChange={handleChange('nickname')}
        />
        <InputField
          type="email"
          label="Enter your email"
          value={logInData.email}
          onChange={handleChange('email')}
        />
        <InputField
          type="password"
          label="Enter your password"
          value={logInData.password}
          onChange={handleChange('password')}
        />
        <button>Submit</button>
      </form>
    );
  };
  
  // InputField.js
  const InputField = ({ type, label, value, onChange }) => (
    <label>
      {label}
      <input type={type} value={value} onChange={e => onChange(e.target.value)} />
    </label>
  );



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




Что еще можно сделать?



Если у вас много форм в проекте, то вы можете вынести рассчет handleChange в отдельный хук useFieldChange:
Код для копирования
  // hooks/useFieldChange.js
  const useFieldChange = setState => fieldName => fieldValue => {
    setState(state => ({
      ...state,
      [fieldName]: fieldValue,
    }));
  };
  // LogInForm.js
  const handleChange = useFieldChange(setLogInData);



Так как это чистая функция (имеется ввиду при первом вызове, потому что возвращает всегда одинаковую функцию), это не обязательно должен быть хук. Но хук смотрится концептуально более правильным и природным решением для React.

Еще можно добавить поддержку callback на месте fieldValue, чтобы полностью повторить поведение обычного setState из React:
Код для копирования
  const isFunc = val => typeof val === "function";

  const useFieldChange = setState => fieldName => fieldValue => {
    setState(state => ({
      ...state,
      [fieldName]: isFunc(fieldValue) ? fieldValue(state[fieldName]) : fieldValue,
    }));
  };



Пример использования с нашей формой:
Код для копирования
  const LogInForm = ({ initialData, onSubmit }) => {
    const [logInData, setLogInData] = useState(initialData);
    const handleChange = useFieldChange(setLogInData);
  
    const handleSubmit = e => {
      e.preventDefault();
      onSubmit(logInData);
    };
  
    return (
      <form onSubmit={handleSubmit}>
        <InputField
          label="Enter your nickname"
          value={logInData.nickname}
          onChange={handleChange('nickname')}
        />
        <InputField
          type="email"
          label="Enter your email"
          value={logInData.email}
          onChange={handleChange('email')}
        />
        <InputField
          type="password"
          label="Enter your password"
          value={logInData.password}
          onChange={handleChange('password')}
        />
        <button>Submit</button>
      </form>
    );
  };



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




О, нет! Не делайте так, пожалуйста!



Формы из замоканых конфигов


Замоканные конфиги — это как webpack конфиг, только для формы.

Лучше на примере, посмотрите на этот код:
Код для копирования
  const Form = () => (
    <form onSubmit={handleSubmit}>
      <InputField
        label="Enter your nickname"
        value={state.nickname}
        onChange={handleChange('nickname')}
      />
      <InputField
        type="email"
        label="Enter your email"
        value={state.email}
        onChange={handleChange('email')}
      />
      <InputField
        type="password"
        label="Enter your password"
        value={state.password}
        onChange={handleChange('password')}
      />
      <button>Submit</button>
    </form>
  );



Некоторым может показаться, что здесь дублируется код, ведь мы же вызываем один и тот же компонент InputField, передавая туда одни и те же параметры label, value и onChange. И они начинают за-DRY-ивать собственный код, чтобы избежать мнимого дублирования.
Часто это делают примерно так:
Код для копирования
const fields = [
  {
    name: 'nickname',
    label: 'Enter your nickname',
  },
  {
    type: 'email',
    name: 'email',
    label: 'Enter your email',
  },
  {
    type: 'password',
    name: 'password',
    label: 'Enter your password',
  },
];

const Form = () => (
  <form onSubmit={handleSubmit}>
    {fields.map(({ type, name, label }) => (
      <InputField
        type={type}
        label={label}
        value={state[name]}
        onChange={handleChange(name)}
      />
    ))}
    <button>Submit</button>
  </form>
);



В итоге с 17 строчек jsx кода получаем 16 строк конфига. Браво! Вот это я понимаю DRY. Если у нас здесь будет 100 таких input-ов, то мы получим 605 и 506 строчки соответсвенно.

Но, как результат, мы получили более сложный код, нарушив принцип KISS. Ведь теперь он состоит из 2-х абстракций: fields и алгоритм (да, алгоритм — это тоже абстракция), который превращает его в дерево React-элементов. При чтении этого кода нам придется постоянно прыгать между этими абстракциями.

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

Этот список можно продолжать долго…

Для реализации этого всегда придется хранить какие-то функции в вашем конфиге, а код, который рендерит конфиг, постепенно превратится в Франкенштейна, принимая кучу разных пропсов и закидывая их по разным компонентам.

Код, который был написан до этого, основанный на композиции компонентов, будет спокойно расширяться, меняться как угодно, при этом не сильно усложнясь благодаря отсутсвию промежуточной абстракции — алгоритма, который строит дерево React-элементов из нашего конфига. Этот код не является дублирующимся. Он просто следует определенному шаблону проектирования, поэтому и выглядит «шаблонным».

Бесполезная оптимизация


После появления хуков в React появилась тенденция оборачивають все обработчики и компоненты без разбора в useCallback и memo. Пожалуйста, не делайте этого! Данные хуки предоставлены разработчиками React не потому, что React медленный и все нужно оптимизировать. Они дают пространство для оптимизации вашему приложению в случае, если вы столкнетесь с проблемами производительности. И даже если вы столкнулись с такими проблемами, не нужно оборачивать весь проект в memo и useCallback. Используйте Profiler для выявления проблем и только потом мемоизацию в нужном месте.

Мемоизация всегда сделает ваш код сложнее, но не всегда производительнее.

Давайте рассмотрим код из реального проекта. Сравните, как выглядит функция с использованием useCallback и без него:
Код для копирования
  const applyFilters = useCallback(() => {
    const newSelectedMetrics = Object.keys(selectedMetricsStatus).filter(
      metric => selectedMetricsStatus[metric],
    );
    onApplyFilterClick(newSelectedMetrics);
  }, [selectedMetricsStatus, onApplyFilterClick]);

  const applyFilters = () => {
    const newSelectedMetrics = Object.keys(selectedMetricsStatus).filter(
      metric => selectedMetricsStatus[metric],
    );
    onApplyFilterClick(newSelectedMetrics);
  };



Читабельность кода явно выросла после удаления обертки, ведь идеальный код — это его отсутсвие.

Производительность этого кода не выросла, ведь эта функция используется так:
Код для копирования
  <RightFooterButton onClick={applyFilters}>APPLY</RightFooterButton>



Где RightFooterButton — это просто styled.button из styled-components, который обновится очень быстро. А вот потребление памяти нашим приложением увеличиться, потому что React всегда будет держать в памяти selectedMetricsStatus, onApplyFilterClick и версию функции applyFilters, актуальную для этих зависимостей.

Если этих аргументов вам недостаточно, прочитайте статью, в которой эта тема раскрыта шире.




Выводы


  • Формы в React — это просто. Проблемы с ними возникают из-за самих разработчиков и документации React, в которой эта тема раскрыта недостаточно подробно.
  • Для удобной работы с вашими компонентами держите ввод и вывод aka value и onChange одного типа. Это позволит вам использовать хук useFieldChange, описанный выше, и получить чуть более многословный, но не мение мощный аналог v-model из Vue для комфортной работы.
  • При написании кода в первую очередь следуйте принципам KISS и YAGNI. А затем уже DRY, но осторожно, чтобы случайно не создать плохую абстракцию.
  • Избегайте описывания ваших форм с помощью конфигов, если такой подход не навязывает ваш фреймворк или библиотека, ведь там алгоритм по превращению конфигов в React-дерево скрыт за их абстракцией.
  • Всегда избегайте оптимизаций, если в них нет явной необходимости.




P.S.


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

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

Подробнее
Реклама

Комментарии 31

    0
    Приятная статья. Добавлю себе.
      0
      useFieldChange получился не очень удобный по сравнению с v-model.
      Вот, например, менее многословный вариант:
      github.com/Lizhooh/react-hooks-input-bind
        0
        Что еще можно сделать?
        Дополню, что можно еще разделить InputField на 2-3 компонента, например так:
          const CustomInput= ({ type, value, onChange }) => (
              <input type={type} value={value} onChange={e => onChange(e.target.value)} />
          );
        
          const FormFieldWrapper = ({ label, children }) => (
            <label>
              {label}
              {children}
            </label>
          );
        
          const InputField = ({ type, label, value, onChange }) => (
            <FormFieldWrapper label={label} >
              <CustomInput type={type} value={value} onChange={onChange} />
            </FormFieldWrapper >
          );

        В FormFieldWrapper обычно не только label, но и текст ошибки валидации, еще что-нибудь. Эту обертку можно использовать для input, select, checkbox, вместо дублирования кода в каждом контроле. Понятно, что в небольших проектах подобное не нужно, но в больших проектах может пригодиться.
        0
        Формы из замоканных конфигов

        Вообще-то если б люди побольше реализовывали нормальную архитектуру, то то, что вы назвали «формой из замоканных конфигов» называлось бы MVP (не тот MVP, а Model-View-Presenter). Разумеется, ваш код на полноценный MVP не тянет, но базовая идея — именно такая. При нормальной реализации позволяет полностью разнести всю бизнес-логику в UI, и унылую визуализацию этого всего посредством какого-нибудь реакта. И при желании потом эту визуализацию переписывать хоть каждый день на новый фреймворк.
          +2

          Когда-то давно я работал с PHP. В то время (надеюсь и сейчас) считалось дурным тоном мешать в одну кучу код и разметку. Правильным же был подход писать или использовать шаблонизаторы. Сейчас в React вижу обратную картину: возврат именно к мешанине кода и разметки, сущность "разметка" разорвана на множество мелких фрагментов, раскиданных по js-файлам.

            +1
            Вся соль в том, что чисто декларативно (т.е. только через html+css) некоторых вещей просто не сделать. И нет практических причин за этим гнаться. Итого визуализация — это не только «разметка», но еще всегда и «код», и это в принципе нормально. Плохо не это, а то, что мало кто задумывается над функциональным разделением кода — того, который нужен для визуализации (и которому самое место быть где-то рядом с разметкой), и кода, который представляет собой ценность для бизнеса (описание предметной области и логику операций на ней).
              0

              Согласен. Поэтому и существуют в разных языках разного рода шаблонизаторы. Да и в том же PHP есть Smarty, кажется весьма популярный среди непрограммистов.

              0

              Незнакомым с тем, о чём вы пишете, приведу пример (т.к. недавно снова на такое наткнулся):


              screenshot


              1. css
              2. html
              3. attributes-css
              4. attributes-javascript
              5. SQL
              6. PHP.

              Для полного счастья не хватило <script/> тега :)

                +1

                Это называется "компонентный подход". Раньше не было удобного способа разбивать код на основе логических связей. Всё что мы могли — это разделить HTML, CSS и JS.

                  0

                  Вы только что полностью передали мое впечатление от реакта.


                  20 лет эволюции замкнулись в круг.


                  В результате каждый проект на реакте, который я видел, содержит заново написанную, неспецифицированную, глючную и медленную реализацию половины Common Lisp (зачеркнуто) angular/vue

                    0
                    Это пол беды.
                    С появлением хуков, они пошли дальше. Все в одну функцию поместили.
                    Посреди компонента-функции, вместе с кодом, который выполняется при каждом обновлении компонента, пишется код, который выполняется при mount, umount и при изменении зависимых переменных.

                    Ну не получается у них сделать шаг вперед без шага назад)
                    Появились пользовательские элементы (компоненты), но в них стали смешивать логику и разметку.
                    Появился более-менее нормальный способ вынесения повторного кода из компонентов (custom hooks), но тут же стали писать логику, хуки, jsx в одной функции.
                      +1
                      Согласен, хуки — не только перемешивание логики и разметки, но еще и мешанина асинхронно выполняемых функций и синхронного рендеринга. Хуки — по факту обычные setTimeout:

                      let prevMyProp = null;
                      function Component({ myProp }) {
                        setTimeout(() => {
                          // запрет выполнения при совпадении параметров, как в хуках [myProp]
                          if (prevMyProp === myProp) return;
                          // логика
                          1 + 1;
                          // для будущей проверки на совпадение
                           prevMyProp = myProp;
                        }, Component.didRenderTimeout)
                      
                        React.useEffect(() => {
                          1 + 1;
                        }, [myProp])
                      
                        return <div />
                      }
                      


                      Но так как Component.didRenderTimeout — динамический параметр, а проверка на изменение пропов — довольно распространенная операция, разработчики сделали хелперы для этого, названные «хуками». Этот шаг по сути небольшой, но он вызвал неоправданный ажиотаж у людей со складом ума «вышло позже — значит надо использовать, все остальное легаси!», которые еще и толпы создают на премьерах новых айфонов) Но у functional components + hooks масса недостатков:

                      • раздувание чистой функции рендера
                      • создание функций, интенсивно использующих замыкания и друг друга (один хук получает информацию из другого хука, который вытаскивает ее из ref, сохраненного в стейте компонента и вызывает setState). Одна-две композиции не принесли бы ощутимого вреда, но я вижу в проектах по пять-десять тесно связанных функций, походит на характерный для jquery клубок-пятисотстрочник.
                      • синтаксис, основанный на «соглашениях». Вместо понятного жизненного цикла (до рождение, после рождения, после обновления и т.п.) с именованными названиями пришли useEffect (использовать эффект?) с неименованными параметрами (первый — асинхронный колбэк, второй — массив с управляющими вызовом асинхронного колбэка элементами), useState (это название годное), который возвращает массив с первым элементом = значение, вторым = функцией для проставления значения...
                      • ухудшение производительности и проблемы с утечками памяти (когда функции создаются непосредственно в рендере и работают с внешними данными)
                      • оформление в functional components и отдельных функциях привело к неструктурированности компонентов и обилию ручных пробросов (контекста и других функций)


                      Таким образом, React сделал шаг в сторону «фреймворкоризации», предложив определенные синтаксические конструкции со своими хелперами (т.е. «соглашения») вместо бытия библиотекой по согласованию переданных данных и DOM-представления. Не смотря на уже 3 довольно объемных проекта, с которыми поработал на хуках+FC, вернулся к классовым компонентам и радуюсь преимуществам:

                      • инкапсулированная логика с методами, в которых уже есть доступ к props, context и другим методам
                      • чистые render-функции
                      • человекопонятный жизненный цикл
                      • простота установки переменных (ref / не ref)
                      • одинаковые функции типа this.handleChange без оборачивания в дополнительный useCallback (бонус — легкость удаления обработчиков вроде addEventListener)
                      • возможность применения декораторов в @-синтаксисе как глобально к классу, так и к методам


                      Вот теперь думаю, что если бы классы вышли после хуков — то побежали ли бы все с криком «о как круто столько преимущество долой эту всю кашу!» переписывать проекты на классы? Почему-то кажется, что да
                        0
                        Почему-то кажется, что да

                        Думаю нет. Писал пару лет на классах, страдал. Перешёл на хуки 2 года назад, долго не мог понять hook-way. Разобрался. Стало значительно удобнее. Возвращаться на классы не планирую.


                        Компоненты на классах плохи не ввиду ключевого слова class, а ввиду того, что в React не сделали никаких удобств по работе с методами жизненного цикла. Одна и та же функциональность вынужденно оказывается размазана по классу и путается с другими. Когда то были миксины, но их выпилили. В итоге если разработчики не сильно круты, а задача не тривиальная, то эти классы, как правило, превращаются в сложноподдерживаемое месиво. Хуки же сгруппировать в древо вызовов весьма тривиально.


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


                        Вы кстати забыли написать про самую хохму. Хуки очень требовательны к пониманию разработчикам основ языка (вроде замыканий, ссылок на объекты) и мемоизации. В то время как на классах относительно успешно мог писать человек который JS видел раньше только на картинке. С хуками он сразу попадёт в множество неочевидных ловушек. Ну и попадают. А хохма в том, что React привнёс хуки ввиду того, что "this это слишком сложно для наших программистов, они не понимают эту концепцию, и мы придумали более простой и элегантный путь"… Который в итоге взрывает мозг даже программистам среднего уровня.


                        Мне самому же хуки очень нравятся. Другая идеология и другие правила игры. В какой-то степени это напоминает решение загадок. Как бы так всё организовать чтобы без лишних рендеров, код был попроще, и всё работало как надо?! Хуки дают ряд инструментов и тасовать их… в общем интересная штука. На классах этого всё было не нужно. Всё было убого (особенно всякие componentWillUpdate), но довольно очевидно.

                          0
                          в React не сделали никаких удобств по работе с методами жизненного цикла. Одна и та же функциональность вынужденно оказывается размазана по классу и путается с другими. Когда то были миксины, но их выпилили.
                          Кстати, я делаю issue в их rfcs репозиторий на эту тему. Изменил структуру классовых компонентов — отделил render от компонента и убрал из компонента пользовательскую логику в массив объектов компонента. То есть логика не размазана по компоненту, а вынесена в отдельные изолированные друг от друга объекты, как хуки.

                          Реализация с примерами использования

                          Введение. Описание, почему предлагаю

                          Описание реализации

                          Не ожидаю, что после этого захотят вернуться к классами, но надеюсь хотя бы некоторые предложения будут реализованы в будущем в каком-либо виде.
                            0
                            Да, в классовых компонентах есть композиционная проблема — если на componentDid(Mount|Update) нужно выполнить несколько разнородных операций (поставить несколько обработчиков, вызвать асинхронную логику), то они указываются одноуровнево, что приводит к раздуванию самого класса. Предлагаемые вами behaviors, как понимаю, позволяют задавать эту логику в отдельных сущностях, которые затем «склеиваются» в основном компоненте, при этом учитывается логика Реакта и проброс в render, что удобнее композиции отдельных классов. Идея, в целом, очевидная, и мне тоже странно, что разработчики Реакта предпочли создать хуки вместо упрощения работы с классами.

                            Согласен с faiwer, что у хуков множество ловушек, и написать низкопроизводительное некрасивое кодом приложение стало намного проще. То, за что я любил Реакт — отсутствие своего мета-языка и строгих правил, очевидность жизненного цикла, простота в целом, с хуками начало уходить и библиотека превращается в «ни туда — ни сюда» — не фреймворк и не библиотека рендеринга.

                            «Месиво в классах»? Вы, наверное, еще не повидали «месиво в хуках», см. скоро в каждом втором проекте. Решается прямыми руками)
                              0
                              Решается прямыми руками)

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


                              Да, месиво в хуках пока не встречал. Стараемся руководствоваться базовыми принципами чистого кода, а их куда проще поддерживать когда нет позвоночника (классов).

                    0
                    По-моему, все действия после первого примера с кодом являются нарушением KISS и YAGNI. Особенно handleChange со строкой в параметрах :)
                    А DRY больше про «не дублируйте логику», а не про экономию места.
                      0
                      Насчет первого, да, так и есть. Идея в том, что мы сначала пишем все отталкиваясь только от KISS. Затем применяем DRY, но, так что-бы абстракции не только переиспользовали логику но и упростили чтение.
                      Cравните 2 примера:
                      e => setLogInData({ ...logInData, nickname: e.target.value })

                      handleChange('nickname')

                      Первый — императивный: возьми данные из события и положи в состояние logInData.nickname.
                      Второй — декларативный: обработай изменение nickname.

                      DRY — именно про «не дублируйте логику», я полностью с вами согласен, я это и пытался донести. В каком месте вам показалось, что я говорю об экономии места? Я это место поправлю.
                      В случае с handleChange переиспользуемой логикой является:
                      Возьми данные и положи в logInData[name]

                      А насчет строки в параметрах, вы просто не привыкли к каррированию :) На мой взгляд, это читается отлично
                      +2
                      Если этих аргументов вам недостаточно, прочитайте статью, в которой эта тема раскрыта шире

                      Несмотря на то, что аргументация статьи в целом примерно правильная, я не могу перестать удивляться тому, как люди всерьёз рассуждают про медленный dependency-checking механизм и лишнюю аллокацию в useCallback, useMemo, Memo, PureComputed. Простая математика мне говорит, что даже если вы покроете вообще всё ваше приложение мемошками и useCallback-ами, и они всегда будут срабатывать зря, вы всё равно не получите ощутимой просадки по производительности. Просто потому, что проверка по ссылке пары значений это околонулевые cost-ы. В то же время как даже 1 лишний рендер на 10 нужных перекроет все эти "потери", т.к. число вызванных аллокаций и проверок в virtualDom радикально превысит все эти мемоизации.


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


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


                      Отдельно не могу не отметить, что "повальной" мемоизации могут быть и другие плюшки:


                      • Если у вас сложным образом устроена работа с данными и, скажем, есть нормализованный стор с иммутабельными данными, которые нужно собирать по частям, то без мемоизации вы легко получите rerender всего приложения разом на любой чих. Неспроста react-redux connect по-умолчанию мемоизирован.
                      • Если у вас и так всё иммутабельно, то повальная мемоизация упрощает дебаг. Вы по-умолчанию исходите из того, что ничего не ререндерится без реальной необходимости. И получив какой-нибудь хитрый баг в хитрых условиях со всякими race-condition его куда проще дебажить, когда у вас вместо 100 render-ов всего 1 :-D

                      Мы используем мемоизацию везде кроме листьев vdom-древа. Во многом с точки зрения дисциплины в коде. И "брат не умер" :)

                        +1

                        Вспомнилось. Кажется Gitlab написан на Vue, ну да не суть. Про сам подход. Бывает приходится проводить code-review какого-нибудь большого сложного рефакторинга. Который просто вынужденно делается одной задачей. А это легко приводит к 6000+ строкам изменений. Да этого следует избегать, но далеко не всегда удаётся.


                        Так вот открывая такой review в gitlab натыкаешься на то, что сам browser работает очень быстро и обрабатывает чудовищное количество domElement-ов играючи. Но как только дело касается SPA, который этим всем заведует, то на любой чих привет фризы по 2-10 секунд. Реальных объективных причин для них нет.


                        Т.е. у ребят 2 проблемы:


                        • Нет механизма постраничной обработки, которая позволит решить эту проблему даже с медленным SPA
                        • Написанный по принципу "wait until the abstraction/optimization is screaming at you" SPA приводит к ужасному usability. Или ещё хуже, как у Discord. Читая ту статью, я испытывал ужасный испанский стыд. Всех этих ошибок ребята могли избежать, если бы включили мозг не спустя много лет существования проекта и мириад жалоб от пользователей, а просто сразу правильно делая. Это всё ведь есть даже в документации.

                        В общем если вы пишете большой и серьёзный продукт, то имхо, лучше если вы везде лишних useCallback и useMemo наставите, которые в половине случаев у вас развалятся, чем если вы будете писать что придётся "wait until the abstraction/optimization is screaming at you".


                        Эти жутко тормозящие SPA уже скорее данность. Чаще всего там проблема в чём-то совсем простом, на что или забили с самого начала, или почему-то даже не знали.


                        P.S., sorry, накипело :)

                        0
                        Честно говоря, не хватает типизации. Типизация снижает количество ошибок. У инпутов нет тип отображения(пароль не должен отображаться как строка). Каждый контрол может иметь индивидульные особенности. Конструкция a => b => c => — уменьшает размер кода, но сильно снижает читаемость. Хотя может дело привычки. Общая функция обработки изменения значения поля, которая определяет какое поле изменилось — для конкретного случая норм. Но она часто будет приносить сложности контролирования ошибок, лучше использовать отсылку на объект поля
                          0
                          Как по мне, то использовать useCallback и useMemo нужно тогда, когда они используются в dependency массивах других хуков useEffect, useCallback и useMemo. В большинстве остальных случаев это не имеет смысла.
                            +1
                            Хорошая, нужная статья, много правильных мыслей, хоть и есть несколько спорных моментов: например, совершенно непонятно зачем приплетён useCallback, который вообще совсем для другого, да и аргументы против генерации формы из конфига довольно странные, т.к. решаемая здесь задача этого вообще не предполагает (к тому же, повторное использование InputField с разными параметрами никак не противоречит DRY). Генерация форм — это вообще совершенно другая задача, которая должна решаться не здесь, а где-то на других уровнях абстракции, если уж вдруг возникнет необходимость в реализации именно такого подхода.

                            Ну и если уж всё это претендует туториал с примерами реального, а не псевдо-кода, то этот код лучше, наверное, сделать рабочим? В начале статьи всё было правильно, но после рефакторинга куда-то потерялся мерж с текущим стейтом.

                            // handleChange курильщика.
                            // В отличии от классового setState(), 
                            // здесь потеряются все данные стейта, 
                            // кроме назначаемого текущего поля. 
                            
                            const handleChange = fieldName => fieldValue => {
                               setLogInData({
                                  [fieldName]: fieldValue,
                               });
                            };
                            


                            // handleChange здорового человека.
                            // Вот тут все будет работать правильно
                            
                            const handleChange = fieldName => fieldValue => {
                               setLogInData({
                                  ...logInData,
                                  [fieldName]: fieldValue,
                               });
                            };
                            
                              0

                              А вообще, по сути говоря, вместо мерджа в 1 state, нужно просто 3 разных useState-а.

                                0
                                конечно, но тут, видимо, сильна ещё инерция старого подхода, используемого в классовых компонентах, с одним общим state и setState()
                                  0
                                  По сути, модель предлагаете делить на 3 независимых поля.
                                  И какие преимущества 3 разных стейтов вместо общего?
                                  Вот недостатки я вижу:
                                  * При обновлении нескольких стейтов за раз, компонент будет обновляться также несколько раз, что как минимум осложнит дебаг.
                                  * дополнительное возня, если все эти стейты нужно передавать в другую функцию. Так как вместо одного объекта.
                                  * в массивы зависимостей хуков вместо одной зависимости придеться добавлять несколько.
                                    0
                                    • При обновлении нескольких стейтов за раз, компонент будет обновляться также несколько раз, что как минимум осложнит дебаг.

                                    It depends. В некоторых контекстах это вызовет ререндер. В некоторых эти изменения группируются. По сути если у вас сложная работа со state-ом, то есть useReducer.


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

                                    Этот аргумент валиден только тогда, когда вам действительно оно нужно. Я могу в противовес сказать, что вы зато не передаёте куда не надо, возможность делать то, что не надо. Это действительно ценно.


                                    в массивы зависимостей хуков вместо одной зависимости придеться добавлять несколько.

                                    А почему вы записали это в минус? Это же жирный плюс. Вы за clean code и best practice или за херак-херак-и-в-продакшн? :)


                                    Преимущества очевидны — мы не мешаем тёплое с красным. Меньше багов, проще код. Лучше разделение ответственностей.


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

                                      0
                                      It depends. В некоторых контекстах это вызовет ререндер. В некоторых эти изменения группируются. По сути если у вас сложная работа со state-ом, то есть useReducer.
                                      Вот как раз в случае форм, я считаю, что автор правильно сделал, что использовал один объект. Хотя, как вы написали, нужно было useReducer использовать.

                                      А почему вы записали это в минус? Это же жирный плюс. Вы за clean code и best practice или за херак-херак-и-в-продакшн? :)
                                      Помимо хуков это уже где-то 10-лет используется, что дробление состояния небольшой сущности/компонента относится к clean code и best practice? Я не спорю, что иногда это нужно и более правильно.

                                      Преимущества очевидны — мы не мешаем тёплое с красным. Меньше багов, проще код. Лучше разделение ответственностей.
                                      Если брать пример формы из статьи, там нет теплого и красного, только теплое. И вы пишите, что лучше теплое отделять от теплого) Я вижу только, что с ростом формы, багов при таком подходе будет как раз больше и код сложнее. Получим кучу состояний, изменения и подписка на изменения которых разбросаны по всему компоненту. Конечно, тут зависит от того, как написать, но думаю, что в большинстве проектов будет именно так.
                                  0
                                  Спасибо за комментарий, я поправил handleChange.
                                  0
                                  del
                                    0
                                    Избегайте описывания ваших форм с помощью конфигов

                                    Вот не согласен максимально. В итоговом варианте с портянкой в разметке получилось, что компонент — черная коробка <Form /> с неизвестным набором полей и полностью скрытой логикой, хранящейся во внутреннем стейте. И вот навскидку то, с чем придется столкнуться при таком подходе:

                                    • Для проброса initialValues в форму придется копаться в реализации и передавать объект с согласованными с именами полей параметрами. Если добавятся propTypes / TS Interface, то это будет дубляж
                                    • Типы передаваемых initalValues, опять же, нужно согласовывать (для селектов / чекбоксов)
                                    • При добавлении влияющей на пропы конкретного поля логики (включение / выключение отображения, disabled, optional, смена label) придется внутри писать массу if-конструкций
                                    • Динамическую раскладку придется поддерживать вручную (чтобы если поле пропало, то следующее встало рядом с ним)
                                    • Хотелось бы посмотреть, как будут реализованы валидации и синхронизация их с backend (когда в ответ присылается к примеру { email: «INVALID»} в слое-контроллере, и нужно вставить эту валидацию в поле в добавление к имеющимся, подсветить его с указанием локализованной ошибки, прокрутить страницу к нему)
                                    • Как будут исключаться поля с пометкой optional из валидаций, а с disabled из отправляемых данных на бэк
                                    • Как будут контролироваться динамически добавляемые поля
                                    • Как сторонние компоненты и другие формы смогут влиять на эту (получать значение, проставлять, менять конфигурацию полей)


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

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

                                    Накипело, столько уже проектов повидал с такими вот формами. Не надо так. Но и превращать конфиг в json-схему генерируемую в cms по созданию форм, разумеется, тоже не надо — это как раз оверинжиниринг, а не создание дополнительного объекта.

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

                                    Самое читаемое