Comments 34
Вот, например, менее многословный вариант:
github.com/Lizhooh/react-hooks-input-bind
Что еще можно сделать?Дополню, что можно еще разделить 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, вместо дублирования кода в каждом контроле. Понятно, что в небольших проектах подобное не нужно, но в больших проектах может пригодиться.
Формы из замоканных конфигов
Вообще-то если б люди побольше реализовывали нормальную архитектуру, то то, что вы назвали «формой из замоканных конфигов» называлось бы MVP (не тот MVP, а Model-View-Presenter). Разумеется, ваш код на полноценный MVP не тянет, но базовая идея — именно такая. При нормальной реализации позволяет полностью разнести всю бизнес-логику в UI, и унылую визуализацию этого всего посредством какого-нибудь реакта. И при желании потом эту визуализацию переписывать хоть каждый день на новый фреймворк.
Когда-то давно я работал с PHP. В то время (надеюсь и сейчас) считалось дурным тоном мешать в одну кучу код и разметку. Правильным же был подход писать или использовать шаблонизаторы. Сейчас в React вижу обратную картину: возврат именно к мешанине кода и разметки, сущность "разметка" разорвана на множество мелких фрагментов, раскиданных по js-файлам.
Незнакомым с тем, о чём вы пишете, приведу пример (т.к. недавно снова на такое наткнулся):
- css
- html
- attributes-css
- attributes-javascript
- SQL
- PHP.
Для полного счастья не хватило <script/>
тега :)
Это называется "компонентный подход". Раньше не было удобного способа разбивать код на основе логических связей. Всё что мы могли — это разделить HTML, CSS и JS.
Вы только что полностью передали мое впечатление от реакта.
20 лет эволюции замкнулись в круг.
В результате каждый проект на реакте, который я видел, содержит заново написанную, неспецифицированную, глючную и медленную реализацию половины Common Lisp (зачеркнуто) angular/vue
С появлением хуков, они пошли дальше. Все в одну функцию поместили.
Посреди компонента-функции, вместе с кодом, который выполняется при каждом обновлении компонента, пишется код, который выполняется при mount, umount и при изменении зависимых переменных.
Ну не получается у них сделать шаг вперед без шага назад)
Появились пользовательские элементы (компоненты), но в них стали смешивать логику и разметку.
Появился более-менее нормальный способ вынесения повторного кода из компонентов (custom hooks), но тут же стали писать логику, хуки, jsx в одной функции.
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)
- возможность применения декораторов в @-синтаксисе как глобально к классу, так и к методам
Вот теперь думаю, что если бы классы вышли после хуков — то побежали ли бы все с криком «о как круто столько преимущество долой эту всю кашу!» переписывать проекты на классы? Почему-то кажется, что да
Почему-то кажется, что да
Думаю нет. Писал пару лет на классах, страдал. Перешёл на хуки 2 года назад, долго не мог понять hook-way. Разобрался. Стало значительно удобнее. Возвращаться на классы не планирую.
Компоненты на классах плохи не ввиду ключевого слова class, а ввиду того, что в React не сделали никаких удобств по работе с методами жизненного цикла. Одна и та же функциональность вынужденно оказывается размазана по классу и путается с другими. Когда то были миксины, но их выпилили. В итоге если разработчики не сильно круты, а задача не тривиальная, то эти классы, как правило, превращаются в сложноподдерживаемое месиво. Хуки же сгруппировать в древо вызовов весьма тривиально.
Ну и да, всего этого можно было бы избежать придумав более адекватное API для классов. Но ведь никто не стал. На них просто забили. Даже множественный контекст и то выпилили. В итоге я с трудом сейчас представляю переход на классы назад, ибо хорошо помню весь этот геморрой.
Вы кстати забыли написать про самую хохму. Хуки очень требовательны к пониманию разработчикам основ языка (вроде замыканий, ссылок на объекты) и мемоизации. В то время как на классах относительно успешно мог писать человек который JS видел раньше только на картинке. С хуками он сразу попадёт в множество неочевидных ловушек. Ну и попадают. А хохма в том, что React привнёс хуки ввиду того, что "this это слишком сложно для наших программистов, они не понимают эту концепцию, и мы придумали более простой и элегантный путь"… Который в итоге взрывает мозг даже программистам среднего уровня.
Мне самому же хуки очень нравятся. Другая идеология и другие правила игры. В какой-то степени это напоминает решение загадок. Как бы так всё организовать чтобы без лишних рендеров, код был попроще, и всё работало как надо?! Хуки дают ряд инструментов и тасовать их… в общем интересная штука. На классах этого всё было не нужно. Всё было убого (особенно всякие componentWillUpdate), но довольно очевидно.
в React не сделали никаких удобств по работе с методами жизненного цикла. Одна и та же функциональность вынужденно оказывается размазана по классу и путается с другими. Когда то были миксины, но их выпилили.Кстати, я делаю issue в их rfcs репозиторий на эту тему. Изменил структуру классовых компонентов — отделил render от компонента и убрал из компонента пользовательскую логику в массив объектов компонента. То есть логика не размазана по компоненту, а вынесена в отдельные изолированные друг от друга объекты, как хуки.
Реализация с примерами использования
Введение. Описание, почему предлагаю
Описание реализации
Не ожидаю, что после этого захотят вернуться к классами, но надеюсь хотя бы некоторые предложения будут реализованы в будущем в каком-либо виде.
Согласен с faiwer, что у хуков множество ловушек, и написать низкопроизводительное некрасивое кодом приложение стало намного проще. То, за что я любил Реакт — отсутствие своего мета-языка и строгих правил, очевидность жизненного цикла, простота в целом, с хуками начало уходить и библиотека превращается в «ни туда — ни сюда» — не фреймворк и не библиотека рендеринга.
«Месиво в классах»? Вы, наверное, еще не повидали «месиво в хуках», см. скоро в каждом втором проекте. Решается прямыми руками)
А DRY больше про «не дублируйте логику», а не про экономию места.
Cравните 2 примера:
e => setLogInData({ ...logInData, nickname: e.target.value })
handleChange('nickname')
Первый — императивный: возьми данные из события и положи в состояние logInData.nickname.
Второй — декларативный: обработай изменение nickname.
DRY — именно про «не дублируйте логику», я полностью с вами согласен, я это и пытался донести. В каком месте вам показалось, что я говорю об экономии места? Я это место поправлю.
В случае с handleChange переиспользуемой логикой является:
Возьми данные и положи в logInData[name]
А насчет строки в параметрах, вы просто не привыкли к каррированию :) На мой взгляд, это читается отлично
Если этих аргументов вам недостаточно, прочитайте статью, в которой эта тема раскрыта шире
Несмотря на то, что аргументация статьи в целом примерно правильная, я не могу перестать удивляться тому, как люди всерьёз рассуждают про медленный dependency-checking механизм и лишнюю аллокацию в useCallback, useMemo, Memo, PureComputed. Простая математика мне говорит, что даже если вы покроете вообще всё ваше приложение мемошками и useCallback-ами, и они всегда будут срабатывать зря, вы всё равно не получите ощутимой просадки по производительности. Просто потому, что проверка по ссылке пары значений это околонулевые cost-ы. В то же время как даже 1 лишний рендер на 10 нужных перекроет все эти "потери", т.к. число вызванных аллокаций и проверок в virtualDom радикально превысит все эти мемоизации.
Т.е. правильнее говорить о том, что обычно вешать повсюду мемоизацию это добровольно усложнять кодовую базу, а это имеет куда более высокую цену, чем та экономия на спичках, о которой идёт речь.
Ну и можно отметить, что для того, чтобы упереться в тормоза вызванные отсутствием мемоизации нужны большие объёмы. Какие-нибудь графики, большие таблицы, большие списки сложных компонент.
Отдельно не могу не отметить, что "повальной" мемоизации могут быть и другие плюшки:
- Если у вас сложным образом устроена работа с данными и, скажем, есть нормализованный стор с иммутабельными данными, которые нужно собирать по частям, то без мемоизации вы легко получите rerender всего приложения разом на любой чих. Неспроста react-redux connect по-умолчанию мемоизирован.
- Если у вас и так всё иммутабельно, то повальная мемоизация упрощает дебаг. Вы по-умолчанию исходите из того, что ничего не ререндерится без реальной необходимости. И получив какой-нибудь хитрый баг в хитрых условиях со всякими race-condition его куда проще дебажить, когда у вас вместо 100 render-ов всего 1 :-D
Мы используем мемоизацию везде кроме листьев vdom-древа. Во многом с точки зрения дисциплины в коде. И "брат не умер" :)
Вспомнилось. Кажется 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, накипело :)
Ну и если уж всё это претендует туториал с примерами реального, а не псевдо-кода, то этот код лучше, наверное, сделать рабочим? В начале статьи всё было правильно, но после рефакторинга куда-то потерялся мерж с текущим стейтом.
// handleChange курильщика.
// В отличии от классового setState(),
// здесь потеряются все данные стейта,
// кроме назначаемого текущего поля.
const handleChange = fieldName => fieldValue => {
setLogInData({
[fieldName]: fieldValue,
});
};
// handleChange здорового человека.
// Вот тут все будет работать правильно
const handleChange = fieldName => fieldValue => {
setLogInData({
...logInData,
[fieldName]: fieldValue,
});
};
А вообще, по сути говоря, вместо мерджа в 1 state, нужно просто 3 разных useState-а.
И какие преимущества 3 разных стейтов вместо общего?
Вот недостатки я вижу:
* При обновлении нескольких стейтов за раз, компонент будет обновляться также несколько раз, что как минимум осложнит дебаг.
* дополнительное возня, если все эти стейты нужно передавать в другую функцию. Так как вместо одного объекта.
* в массивы зависимостей хуков вместо одной зависимости придеться добавлять несколько.
- При обновлении нескольких стейтов за раз, компонент будет обновляться также несколько раз, что как минимум осложнит дебаг.
It depends. В некоторых контекстах это вызовет ререндер. В некоторых эти изменения группируются. По сути если у вас сложная работа со state-ом, то есть useReducer.
дополнительное возня, если все эти стейты нужно передавать в другую функцию. Так как вместо одного объекта.
Этот аргумент валиден только тогда, когда вам действительно оно нужно. Я могу в противовес сказать, что вы зато не передаёте куда не надо, возможность делать то, что не надо. Это действительно ценно.
в массивы зависимостей хуков вместо одной зависимости придеться добавлять несколько.
А почему вы записали это в минус? Это же жирный плюс. Вы за clean code и best practice или за херак-херак-и-в-продакшн? :)
Преимущества очевидны — мы не мешаем тёплое с красным. Меньше багов, проще код. Лучше разделение ответственностей.
По сути вот как раз группировать нужно тогда, когда возникла в этом потребность. И даже в этом случае хорошо бы задуматься над тем, как бы не тащить лишнее туда, куда не надо.
It depends. В некоторых контекстах это вызовет ререндер. В некоторых эти изменения группируются. По сути если у вас сложная работа со state-ом, то есть useReducer.Вот как раз в случае форм, я считаю, что автор правильно сделал, что использовал один объект. Хотя, как вы написали, нужно было useReducer использовать.
А почему вы записали это в минус? Это же жирный плюс. Вы за clean code и best practice или за херак-херак-и-в-продакшн? :)Помимо хуков это уже где-то 10-лет используется, что дробление состояния небольшой сущности/компонента относится к clean code и best practice? Я не спорю, что иногда это нужно и более правильно.
Преимущества очевидны — мы не мешаем тёплое с красным. Меньше багов, проще код. Лучше разделение ответственностей.Если брать пример формы из статьи, там нет теплого и красного, только теплое. И вы пишите, что лучше теплое отделять от теплого) Я вижу только, что с ростом формы, багов при таком подходе будет как раз больше и код сложнее. Получим кучу состояний, изменения и подписка на изменения которых разбросаны по всему компоненту. Конечно, тут зависит от того, как написать, но думаю, что в большинстве проектов будет именно так.
Избегайте описывания ваших форм с помощью конфигов
Вот не согласен максимально. В итоговом варианте с портянкой в разметке получилось, что компонент — черная коробка <Form /> с неизвестным набором полей и полностью скрытой логикой, хранящейся во внутреннем стейте. И вот навскидку то, с чем придется столкнуться при таком подходе:
- Для проброса initialValues в форму придется копаться в реализации и передавать объект с согласованными с именами полей параметрами. Если добавятся propTypes / TS Interface, то это будет дубляж
- Типы передаваемых initalValues, опять же, нужно согласовывать (для селектов / чекбоксов)
- При добавлении влияющей на пропы конкретного поля логики (включение / выключение отображения, disabled, optional, смена label) придется внутри писать массу if-конструкций
- Динамическую раскладку придется поддерживать вручную (чтобы если поле пропало, то следующее встало рядом с ним)
- Хотелось бы посмотреть, как будут реализованы валидации и синхронизация их с backend (когда в ответ присылается к примеру { email: «INVALID»} в слое-контроллере, и нужно вставить эту валидацию в поле в добавление к имеющимся, подсветить его с указанием локализованной ошибки, прокрутить страницу к нему)
- Как будут исключаться поля с пометкой optional из валидаций, а с disabled из отправляемых данных на бэк
- Как будут контролироваться динамически добавляемые поля
- Как сторонние компоненты и другие формы смогут влиять на эту (получать значение, проставлять, менять конфигурацию полей)
Не спорю, можно, если очень захотеть, написать все это внутри одного компонента в портяночном («единосущностном») формате без дополнительных абстракций, но когда-нибудь все равно придется сделать вторую форму. И либо скопировать все туда, либо понять, что однотипные конфиги и общие утилиты позволяют намного проще решать все задачи, связанные с формами, и позволяют получать достаточное представление о форме контроллерам, другим компонентам, системе валидации, синхронизированной с ответом от бэка. А хранение данных (значений полей, валидаций) в общедоступных сторах вместо скрытого локального стейта позволяет вывести одновременно степень автоматизации и степень контроля приложения на новый уровень.
В общем, я бы не использовал описанный в статье подход даже для таких казалось бы маленьких и супер-простых форм, потому что в продакшене будет совсем другая история, и следовало бы начать с проектирования грамотного конфига. Фичи в него можно добавлять постепенно и обрабатывать централизованно с минимальным вмешательством непосредственно в компоненты уже сделанных форм. Кроме того, убежден, что грамотно спроектированный «скелет» (с глобальным стейтом, механизмом валидаций, конфигом полей, оптимизацией рендеринга) не является оверинжинирингом, а служит базой для масштабирования, под какими бы yarniенками ни маскировался подход «слепить по минималке и перекопировать по сотне раз с разной реализацией», в будущем все равно кому-то придется сделать нормально.
Накипело, столько уже проектов повидал с такими вот формами. Не надо так. Но и превращать конфиг в json-схему генерируемую в cms по созданию форм, разумеется, тоже не надо — это как раз оверинжиниринг, а не создание дополнительного объекта.
Только сейчас прочитал ваш комментарий.
React — библиотека для создания пользовательских интерфейсов. Конфиг — это дополнительный слой абстракции который описывает страктуру нашей формы. Использование конфига для описывания структый является не природным для React, поскольку в React для этого есть render функции, jsx и эти интрументы прекрасно справляются со своей задачей.
Если вы написали конфиг вам все-равно придется написать функцию, которая превращает конфиг в react элементы. Постепенно вы, или ваши колеги, добавите в конфиг кучу полей которые будут управлять рендерингом этого же самого конфига и придумаете свой фрейморк, в котором можно будет разобраться только прочитав исходный код — ту самую функцию которая преращает конфиг в дерево React-элементов. А код этой функции неизбежно будет сложным, ведь он проделывать целую кучу всяких проверок и предположений для каждого элемента из конфига а потом исходя из этого рендерит компоненты с абсолютно разными prop-ами.
Всего этого можно избежать описываю структуру напрямую в рендере. Вы думайте что это как-то кординально все меняет, но это не так. Это все тот же конфиг только записанный в виде JSX и вы сразу, напрямую, управлете рендерингом элементов в нем, избегая дополнительных неявных связей и сложного кода для рендерига в итоге.
Дальше по пунктам:
- initialValues, interface, propTypes это то, с чего нужно начинать разработку формы, в этом случае копаться в реализации при их добавлении не придется. Если же это проект в котором не используется ни одно ни второе, что печально, то копаться в любом случае придется
- Не совсем понимаю, о каких initalValues для select/checkbox идет речь, у нас ведь контролируемая форма. Как я писал выше типы value и onChange для UI компонентов стоит держать одного типа в этом вам поможет TS, просто будет ругаться если передадите что-то не то. Я так и не понял в чем здесь проблема
- Не знаю как статический конфиг помогает вам делать динамическую раскладку, возможно это какой-то фреймворк вам помогал с этим, но не конфиг. Все что касается динамики в render функциях делать на много удобнее чем в статическом массиве объектов
- При использовании конфигов if конструкции никуда не денутся, они просто останутся в функцию которая рендерит конфиг, что сильно ее усложнит ведь там плотность этих if конструкций будет зашкаливать. Условный рендериг абсолютно нормально и удобно делается в React, вот статья в доке. А насчет disabled так вообще никаких проблем, просто передайте его пропом в свой компонент
- Как я писал в начале статьи валидацию эта статья не покрывает просто что-бы не быть слишком длинной. Но если в 3-х словах, то мы просто создаем еще одно состояние для ошибок и когда сервер возвращает ошибку или клиентская валидация возвращает ошибку мы сетим ошибку в поле с нужным именем и тектом ошибки, этот текст передаем в компонент который рендерит ошибку (обычно это просто prop error в нашем InputField). То есть у нас есть просто чистая функция которая принимает данные и возвращает ошибки. Аналогично тому что описано в документации популярной библиотеки для работы с формами в React Formik
- Ни каких пометок optional на полях формы нам не нужно, функция валидации сама знает какие поля обезательные, какие нет, и какие еще проверки нужно сделать для каждого поля
- Если под «динамически добавляемыми полямы» вы имеете ввиду массив с полями формы, то можете посмотреть здесь. Если нет, то это обычный условный рендеринг
- «Сторонние компоненты и другие формы смогут влиять на эту» очень просто, это ведь обычный React компонент и у него есть props, context(не рекомендую), redux и все что вы сможете придумать
Конечно могут быть случаи, когда вам нужно писать конфиг. Например если вы пишите какой-то конструктор для форм, формы должны сериализироваться и десериализироваться или используется готовая библиотека работает с таким форматом и вы ее любите, то это имеет смысл. Тем не мение в туториалах популярных библиотек для React: Formik, Final Form для описывания структуры формы используются render функции и нигде не рекомендуется использовать конфиги.
Для меня Реакт — библиотека, синхронизирующая состояние между js-стейтом и DOM с бонусом в виде жизненного цикла компонентов. JSX в качестве хранилища стейта крайне неудобен ввиду сложности сериализации-десериализации, он является лишь описанием, как js-структуры корректно перевести в DOM.
Насчет кучи проверок и ифов в конструкторе форм из конфига — вы явно перегибаете, их будет совсем немного при грамотном проектировании, а в большинстве случаев — ни одной:
{formConfig.map(({ FieldComponent, fieldProps }) => <FieldComponent {...fieldProps}/>)}
, но при динамической раскладке сюда добавится дополнительный слой в виде группировки полей из конфига по определенному признаку и их выведению в соответствующих размеру экрана рядах, но это несколько дополнительных строк, и уж точно «дополнительных фреймворков» не нужно.If-конструкции уходят практически полностью благодаря переносу в конфиг в виде семантических параметров — isDisabled, isShown, isOptional, и их можно менять динамически в удобной манере:
formConfig.phone.isOptional = formConfig.email.isValid() || formConfig.name.isDisabled || true
, и зашивать это статично в валидатор — плохая идея. При сабмите формы легко можно пробежаться по isDisabled и исключить эти поля и из отправки на бэк, и из валидаций.Через пропы влиять на jsx — путь к раздуванию компонентов и рассинхрону апи у однородных сущностей. А вот добавить параметр к унифицированному компоненту и сделать изменение в одной точке — что может быть проще? Если компонент рендеринга форм начинает выглядеть сложно — это композиционная проблема, а не проблема подхода.
В целом для простейших форм и если их до десятка на проекте, можно и по принципу «черного ящика» создавать компоненты и с локальным стейтом, но в перспективе все равно придется создавать унифицированное решение с более открытым интерфейсом.
В предыдущем ответе у меня была ссылка на небольшую форму
https://codesandbox.io/s/github/VladislavMurashchenko/use-prop-change-sandbox?file=/src/containers/UserForm.tsx
Интересно было бы посмотреть на ваш вариант с использованием конфига.
Разработка формы на React. Принципы KISS, YAGNI, DRY на практике