Комментарии 22
Боюсь вы тут сделали неправильно всё, что только можно.
В библиотеку работы с формами зачем-то засунули отправку их по http.
Используете поиск по дому для поиска обязательных полей. Причём делаете это некорректно. Банальную текстарию вы так не найдёте. Про кастомные контролы и говорить не приходится.
Повреждаете пользовательские данные при сохранении вырезая "не нужные" символы.
Сообщения касающиеся конкретного поля отображаются вдалеке от него.
Валидация происходит лишь после заполнения и отправки всей формы.
Захардкодили в библиотеку проверку совпадения полей. Это если и нужно, то в 1-2 форме на весь проект. Но даже в них это UX-антипаттерн.
Вешаете стили на теги.
Выводите одно и то же сообщение для любых ошибок валидации.
Собираете имена классов из строк.
Во время запроса форма вообще пропадает, а потом снова показывается, чтобы показать ошибку.
спасибо за критику, жду вашей версии хука
0. Выбрали для реализации Реакт, где классы эмулируются на хуках, а для простейшей формы приходится обмазываться костылями.
А зачем в JS классы, если объекты можно создавать без них?
Не считая, конечно, случая когда кто-то перешел из другого языка и не может без классов.
А зачем их эмулировать, если можно использовать нативные синтаксические конструкции?
Классы эмулируются на хуках? Что вы имеете ввиду? Хуки - это обычные функции. Современный React полностью функциональный, если можно так выразиться. Кроме того, к вашему сведению, в JS классов как таковых не существует. Класс в JS - это разновидность функции, обертка (синтаксический сахар) над функцией-конструктором. Так что эмуляция классов с помощью функций - это вполне в духе JS
Хуки - это функции с сайд эффектами. К ФП они имеют такое же отношение, как морские свинки к морю и свинкам.
Комбинация функции-конструктора и прототипа - это и есть класс. Недавно лишь позволили на уровне синтаксиса называть вещи своими именами. Обычным функциям (не классам) прототипы нафиг не нужны. Именно поэтому, например, у стрелочных функций вы никакого прототипа уже не найдёте и создавать с их помощью объекты не сможете.
Комбинация функции-конструктора и прототипа - это и есть класс
В большенстве случаев, почти во всех, нам не нужны прототипы. Достаточно просто функций которые создают объекты. А иногда можно и сразу создать объект синглтон без всяких функций и классов.
Хуки - это функции с сайд эффектами.
Это не совсем так. Хуки позволяют общатся с ядром реакта, мы не можем делать сайд эффекты в хуках, но можем попросить реакт сделать сайд эффект за нас.
Ну да, для каждого свойства по отдельному объекту - это очень прям нужно в большинстве случаев.
Ох уж эта словесная эквилибристика. Мы и переменной значение присвоить не можем, а можем лишь попросить JS движок сделать это за нас.
Наверное, вы хотели сказать, для каждого объекта по отдельному свойству? Эти оптимизации с экономией памяти не нужны в большинстве случаев.
Я имел ввиду, что напрямую сайд эффекты делать нельзя. Правила такие же как в компоненте. Компонент это ведь не просто функция с сайд эффектами. А значит хуки не равно функции с сайд эффектами.
В JavaScript классы существуют. Просто они не являются эквивалентами для классов из других языков. К тому же es6-классы - "не просто синтаксический сахар"(перевод).
Эмуляция объекта работает однозначно медленее, чем объект на базе прототипа. А по поводу хуков я согласен с 0м сообщением.
Хуки сделали для того, чтобы ещё снизить порог вхождения, но низкий порог вхождения чреват тем, что появляется куча народу "знающих" как надо программировать.
Класс даёт очень хорошие возможности по инкапсуляции. В итоге всё в одном месте. Функциональный же подход привёл лишь к тому, что постоянно есть куча функций, а когда их становится сотни и более разбираться становится ну очень не просто.
Да и хуки привели лишь к тому, что цикл работы компонента стал ещё менее понятен и куда хуже поддаётся контролю.
В итоге сделать прототипчик проще, заставить его хорошо работать стало куда сложнее.
А вы смотрели исходный код React? Ни одного класса, только функции. Как же они без классов-то справляются? Или им, по-вашему, инкапсулировать нечего? Я писал на "классовом" React, сейчас пишу на "функциональном" и нисколько не жалею об отсутствии классов со всеми их this, bind и прочим. И не кажется ли вам, что "все в одном месте" противоречит самой сути компонентной разработки?
Хуки тоже дают инкапсуляцию на самом деле. То, что они принимают и возвращают - это публичный интерфейс из данных и методов для работы с ними, а что они скрывают внутри - вызывающий код и знать не знает. Другое дело что да, удобство хуков по сравнению с классами работает хорошо до тех пор, пока этих самых вызовов хуков немного. И пока в них направо и налево не начинают передаваться коллбеки. Поэтому я, например, стараюсь избегать использование всяких там useEffect/useCallback и прочих в компоненте напрямую, оборачивая их во что-то более удобоваримое (кастомные хуки/HOC)
Вообще, товарищ @nin-jinвысказал все очень правильно
Вы намешали в кучу всего что можно и выглядит это трешово
Как мне сделать разные сообщения на разные ошибки? Сколько будет стоить расширить хук кастомным контролом? Могу я опустить последний параметр в хуке если перекрою onSubmit? Зачем делить messages и validators, выглядит так, будто они связаны. Какие поля должны быть в initialData? Обязательно все перечислять?
Вы проигнорили все 10 пунктов, что перечислил товарищ выше, может ответите по ним что нибудь? Вы действительно советуете юзать Вашу либу для форм?
Дополню ещё, что мало того, что в либу с формами добавили http зачем то, так еще и зависимость от сторонней либы =/
Спасибо за статью и ваш труд.
Но я не считаю решение описанное здесь гибким и универсальным.
Вы создали один хук который умеет делать все на свете, а коду который его использует нужно подстраиваться под него. При этом часть функционала может быть не нужна в каждой форме. Например reset.
На мой взгляд, лучше создать много маленьких хуков и утилит и использовать в каждой из форм только те, которые нужны конкретно в ней. При этом само состояние я бы оставил под контролем самой формы, чтобы можно было отказаться от использования хука и написать кастомный обработчик в случае необходимости.
То что вы делаете с input-ами, выражая их через массив обычных объектов и рендеря через map в своей статье я называл "формы из замоканных конфигов". И я по прежнему считаю это антипаттерном. Вот ссылка на мою статью о формах:
https://habr.com/ru/post/495518/
Еще один пример моего подхода можно посмотреть здесь:
https://codesandbox.io/s/github/VladislavMurashchenko/use-prop-change-sandbox?file=/src/containers/UserForm.tsx
К сожелению, пока нет примера валидации.
Спасибо за комментарий.
Давайте по порядку.
1. "Вы создали один хук который умеет делать все на свете...".
Во-первых, не все на свете, а только работать с формами. Во-вторых, разве кастомные хуки разрабатываются не для того, чтобы повсюду "переиспользоваться" без необходимости переписывания для конкретного кейса?
2. "При этом само состояние я бы оставил под контролем самой формы...".
Пожалуйста, реализуйте собственную функцию "onSubmit" и передайте ее в хук, и будет вам "кастомный обработчик".
3. "И я по прежнему считаю это антипаттерном...".
Ну, а я считаю это хорошим паттерном)
Да, я читал вашу статью, интересная.
Во-первых, не все на свете, а только работать с формами.
Да, но я немного о другом. Есть 2 подхода к переиспользованию кода.
1) Написать один хук который принимает большой конфиг и может делать абсолютно все, что связано с формами. А потом мы используем его в каждой нашей форме, берем только половину от возможностей, какие-то вещи переопределяем или используем хаки вроде setFieldValue
из Formik. В итоге складывается впечатление, что инструмент не помогает, а мешает в некоторых ситуациях.
2) Создать много маленький хуков и утилит которые хорошо сочетаются между собой и собирать разные формы используя их как строительные блоки, которые стараются помочь, но не диктуют архитектуру и не ограничивают, так как любой из блоков можно легко выкинуть и заменить кодом который делает что-то вручную. При этом, эти строительные блоки не догадываются о существовании друг друга, если какой-то из них недостаточно хорош для текущей задачи, остальные все же полезны. В худьшем случае ниодна из утилит не подойдет и мы напишем абсолютно все вручную, но они хотя бы не мешают
На самом деле есть ещё и 3 способ: Не пытаться предусмотреть всё на свете, но предоставить разумное поведение по умолчанию с возможностью настроить поведение любого аспекта под себя. Но на Реакте это сделать сложно, поэтому обычно получается (1), потом разработчики упарываются по (2), на базе которого всё-равно потом собирают (1), ибо каждый раз собирать типовую форму из микро-кирпичиков задалбывает.
Тоже делал что-то подобное только несколько иначе. Что мне не нравится в предложенной реализации (уже обо многом сказали выше):
Отправкой формы и то, как это реализуется (урл и т.д.) - это не задача этого хука, нарушается SRP.
Форма собирается из массива данных - тут две проблемы: во-первых мешаются в кучу данные и представление. Я предпочитаю, когда представление реализуется в jsx/html-темплейте, а не в Json-е. Иначе такой код очень трудно воспринимать. Во-вторых, подумайте что вы будете делать, если некоторые ваши инпуты перестанут быть обычными инпутами, а станут селектами, мультиселектами, текстареями, автокомплитами и т.д. Во что превратиться ваша конфигурация такой формы и как дорого вам обойдется добавление нового типа поля? А если бы вы просто собирали вашу форму в jsx и подвязывали все нужные пропсы ручками - реализовали бы все что угодно без лишней головной боли
Валидация может быть какая угодно, не только required/minLength и т.д. Если мы настраиваем модель валидации, и получаем объект с ошибками валидации, то мы должны учесть любые кастомные правила и то, что у одного поля может быть несколько ошибок валидации, которые неплохо бы отобразить одновременно
Я тоже делал что-то подобное на TypeScript. Там я описывал саму модель, схему валидации, например:
interface SoilLayerVM {
soil_id: string;
depth: string;
}
const validationScheme: ValidationScheme<SoilLayerVM> = {
soil_id: {
required: Validators.required
},
depth: {
required: Validators.required,
number: Validators.float,
range: Validators.range(0, 1000)
}
};
И тоже делал похожий хук:
const { errors, setFormField, validate } = useForm(model);
Вызывал validate когда нужно (здесь onValidate - это кастомный проп, не то же самое что одноименный HTML-атрибут, ну да не важно, можно в onSubmit если хочется)
<Form onValidate={() => validate(validationScheme)} ...>
а errors, setFormField подвязывал к компонентами вручную, типа
<FormTextField label="Глубина, м" value={model.depth} onChange={e => setFormField('depth', e.target.value)}
error={!!errors?.depth} errorHintComponent={
<>
{errors?.depth?.required && <FormHintText>Поле является обязательным</FormHintText>}
{errors?.depth?.number && <FormHintText>Введенное значение не является числом</FormHintText>}
{errors?.depth?.range && <FormHintText>Значение должно быть в диапазоне [0-1000]</FormHintText>}
</>
} />
React: немного о работе с формами