
Это третья статья из цикла про наш фреймворк Steroids: в ней мы рассказываем о том, как пришли к созданию собственного движка форм для React. Если вы ещё не читали предыдущие материалы, рекомендуем с ними ознакомиться:
Почему мы решили создать свои формы?
Изначально для работы с формами мы использовали библиотеку Redux-Form. Она обеспечивала автоматическое обновление состояния, валидацию и другие полезные функции.
Со временем мы упёрлись в проблемы производительности. В сложных формах на React и React Native постоянно сталкивались с лишними перерендерами и фризами страницы. К тому же Redux-Form перестала поддерживаться автором. Поэтому настал момент переехать на другое решение или написать свой движок. Для этого предварительно выделили основные пункты, которые нам нравились в Redux-Form, и сформулировали то, что мы хотим от собственного решения.
Сохранить предыдущее API
На тот момент у нас уже было много проектов, которые нужно было поддерживать. Чтобы переход прошёл максимально безболезненно, требовалось сохранить прежнее API.
Иметь возможность использовать как глобальный, так и локальный стейт
Хотелось, чтобы форма могла работать и с глобальным redux-хранилищем — для сложных wizard-форм, и с локальным состоянием — для более быстрого рендера.
Иметь возможность рендерить поле разными способами
Формы в проектах бывают разные, и одного способа рендера полей оказалось мало — в итоге у нас получилось три варианта для рендера поля.
Обычным компонентом поля (InputField, DropDownField, TextField и тд):
<InputField attribute='name' label='Имя' />
Применяется, когда очевидно, какое поле нужно отрендерить.
Передавать сам компонент в обертку <Field/>:
<Field attribute='name' label='Имя' component={InputField} />
Данный способ применяется, когда мы заранее не знаем, какое поле нужно отрендерить или оно может меняться, в зависимости от условий.
Передавать название компонента в виде строки:
<Field attribute='name' label='Имя' component='InputField' />
Для нас это важно, так как мы рендерим динамические формы на основе мета-модели, которая приходит с бэкенда.
Иметь возможность использовать поля вне формы
То есть не оборачивать их в <Form/>, а использовать поле как контролируемый компонент, передав value и onChange.
<InputField attribute='foo' label='FooName' value='1' onChange={(value) => console.log('Value:', value)} />
Сформулировав требования и попробовав популярные библиотеки, мы поняли, что лучший для нас вариант — написать собственный движок.
Как устроены Steroids Form
В этой главе мы разберём, как устроены Steroids Form «под капотом».
На схеме показана последовательность вложенных компонентов на примере InputField — от общей обёртки Form до конкретного поля ввода InputField.

Form
Основой любой формы в Steroids выступает компонент Form. Он отвечает за:
инициализацию провайдера данных формы;
обработку ошибок;
вызов колбэков (
onSubmit,onErrorи другие);синхронизацию значений формы с адресной строкой;
установку начальных значений.
Внутри компонента Form создается контекст, внутри которого хранятся:
уникальный идентификатор формы, позволяющий сохранять и получать данные;
модель конфигурации полей, на основе которой происходит рендеринг;
провайдер данных формы, использующий в качестве хранилища либо React Reducer, либо Redux;
Важно: сам контекст не хранит все значения формы напрямую. В нём находятся только методы и инструменты для работы со значениями и состоянием.
Режимы работы с данными формы
Форма может работать с двумя хранилищами данных:
Локальный (React Reducer) — подходит для простых форм, где данные нужны только внутри текущего компонента.
Глобальный (Redux) — используется в случаях, когда значения формы должны сохраняться между экранами (например, в пошаговых формах) или быть доступны извне.
Благодаря общему для обоих хранилищ интерфейсу провайдера, в обоих режимах доступны одни и те же утилиты и хуки:
useForm— используется внутриForm.tsx. Позволяет получить текущее состояние формы: ошибки, количество сабмитов, признак отправки и метод для сохранения ошибок.useField— используется вfieldWrapper. Возвращает значение конкретного поля, ошибки и метод для изменения значения.select— метод для получения значений формы напрямую.
FieldWrapper
Каждое поле формы оборачивается в fieldWrapper. Он выполняет сразу несколько задач:
определяет, какой UI-компонент и с какими параметрами нужно отрендерить, исходя из модели и переданных данных;
из контекста получает данные провайдера: текущее значение поля, ошибки и метод изменения значения;
передаёт эти данные в компонент поля, в нашем случае в
InputField;оборачивает это поле в
FieldLayout, который отвечает за отображение лейбла, подсказок, обязательности и ошибок.
Компонент поля (InputField)
Компонент поля находится на самом нижнем уровне. Он отвечает за собственную бизнес-логику (например, валидацию формата данных или специфическое поведение поля). При этом он не управляет получением и сохранением значений — эта базовая логика полностью передаётся ему через fieldWrapper.
Использование форм в Steroids
Теперь давайте на примерах посмотрим, что умеет Steroids Form.
Создание простой формы
Форма авторизации может быть описана всего парой компонентов. При успешной отправке значений выводим уведомление:
import React from 'react'; import {Form, EmailField, PasswordField, Button} from '@steroidsjs/core/ui/form'; import {useDispatch, useBem} from '@steroidsjs/core/hooks'; import {showNotification} from '@steroidsjs/core/actions/notifications'; import './LoginFrom.scss'; export default function LoginFrom() { const bem = useBem('LoginFrom'); const dispatch = useDispatch(); return ( <div className={bem.block()}> <Form onSubmit={() => { dispatch(showNotification(__('Успешно!'))); }} > <EmailField attribute='email' label='Email' required /> <PasswordField attribute='password' label='Пароль' required security /> <Button type='submit' label='Войти' /> </Form> </div> ); }
Задание начальных значений
Чтобы предзаполнить поля формы, достаточно передать объект в initialValues. Ключи должны совпадать с attribute у полей:
<Form initialValues={{ email: 'john@gmail.com', }} > <EmailField attribute='email' label='Email' required /> <PasswordField attribute='password' label='Пароль' required security /> <Button type='submit' label='Войти' /> </Form>
Хранение состояния в Redux
По умолчанию состояние формы хранится внутри самого React (через useReducer). Если же нужно иметь доступ к данным формы по всему проекту, например выводить в шапке сумму заказа, исходя из значений формы, то в этом случае удобнее подключить глобальное хранилище с помощью флага useRedux.
const ORDER_FORM_ID = 'OrderFrom'; // идентификатор формы в Redux export default function OrderForm() { return ( <Form formId={ORDER_FORM_ID} useRedux > <NumberField attribute='count' label='Количество' type='number' /> <NumberField attribute='price' label='Цена за единицу' /> </Form> ); }
Обратите внимание, что мы указали attribute для каждого поля формы. Значения таких полей записываются в хранилище, и их можно получить из любого места приложения через getFormValues:
export default function OrderSummary() { const { count = 0, price = 0 } = useSelector(state => getFormValues(state, ORDER_FORM_ID)); const total = count * price; return ( <div> Итог: {total} ₽ </div> ); }
Динамическая конфигурация полей через массив
Список полей можно передать не только JSX-разметкой, но и динамически, при помощи массива объектов. Данный способ применяется когда внешний вид, поведение или даже состав полей может меняться в зависимости от тех или иных условий. Например, поле «Адрес» в форме для создания профиля сотрудника может выглядеть по-разному:
если сотрудник работает в офисе — это будет поле со списком доступных офисов,
если сотрудник работает удалённо — обычное текстовое поле для ввода адреса.
В таком кейсе удобнее передать конфиг формы, который отрендерится с помощью Field:
const EMPLOYEE_FORM_ID = 'EmployeeFormId'; export default function EmployeeForm() { const {isRemote} = useSelector(state => getFormValues(state, EMPLOYEE_FORM_ID)); const fields = [ { attribute: 'fullName', label: 'ФИО', required: true, component: InputField, }, { attribute: 'email', label: 'E-mail', required: true, component: EmailField, }, { attribute: 'isRemote', label: 'Планируете работать удаленно?', required: true, component: RadioField, }, { attribute: 'address', label: 'Адрес сотрудника', component: isRemote ? TextField : DropDownField, items: !isRemote && [ { id: 1, label: 'Москва, Ленинградский проспект 10', }, { id: 2, label: 'Санкт-Петербург, Невский проспект 25', }, ], }, ]; return ( <Form formId={EMPLOYEE_FORM_ID} fields={fields} onSubmit={(values) => console.log('Отправлено:', values)} /> ); }
Интеграция с бэкендом
Form умеет сам отправлять данные на сервер. Достаточно передать action (URL) и метод запроса. Причем action может быть как строкой, так и функцией:
<Form formId={FORM_ID} fields={formFields} action='/api/v1/auth/login' actionMethod='POST' useRedux onComplete={(values, data) => { if (data.accessToken) { dispatch(login(data.accessToken, ROUTE_USERS)); } }} submitLabel='Войти' />
В этом примере после получения данных от сервера Form вызовет функцию onComplete со значениями формы и данными ответа.
Жизненный цикл и обработчики событий
Кроме onComplete, есть еще несколько полезных колбэков жизненного цикла формы.
onBeforeSubmit — срабатывает при отправке формы, позволяет, к примеру, добавить кастомную проверку и предотвратить запрос:
const onBeforeSubmit = (data) => { if (!data.email) { dispatch(formSetErrors(FORM_ID, { email: [__('Введите корректный email.')], })); return false; } return data; };
onAfterSubmit — срабатывает после завершения запроса, даже если сервер вернул ошибку:
const onAfterSubmit = (values, response) => { if (response.statusCode === 400) { dispatch(showNotification(response.errors.message)); } };
onError — срабатывает при сетевых или серверных ошибках:
const onError = (requestError) => { if (requestError instanceof GlobalError) { throwAsyncError( new GlobalError(requestError), ); } };
Валидация и ошибки
По умолчанию ошибки приходят от сервера в объекте errors, и Form сам показывает их под соответствующими полями.
<Form> <EmailField attribute='email' label='Email' required /> <PasswordField attribute='password' label='Пароль' required security /> <Button type='submit' label='Войти' /> </Form>
При получении следующего объекта errors, сообщение с ошибкой сразу появится под полем пароля:
{ "statusCode": 400, "errors": { "password": "Неверный пароль" } }
Если же нужно вывести общее сообщение об ошибке в виде тоста, можно использовать проп submitErrorMessage:
<Form submitErrorMessage='Упс, что-то пошло не так' /* ... */ />
Синхронизация с адресной строкой
При разработке каталога товаров или любого списка данных с множеством фильтров, удобно хранить значения формы прямо в URL, чтобы восстановить их после перезагрузки. Кроме того, это позволяет поделиться ссылкой на текущий вид каталога со всеми фильтрами. Для этого в Steroids Form есть флаг addressBar:
<Form formId={FORM_ID} fields={formFields} useRedux addressBar submitLabel='Войти' />
Таким образом, Steroids Form закрывает все наши стандартные сценарии работы с формами: от локального состояния и валидации до интеграции с Redux и API. Более подробное описание пропсов формы и примеры использования можно найти в документации.
Сравнение с другими библиотеками
Мы не стали замыкаться только на своём решении, поэтому сравнили Steroids Form с другими библиотеками для работы с формами в React. Взяли несколько критериев, которые, на наш взгляд, наиболее важны:
Производительность — насколько быстро библиотека работает на больших формах, как она оптимизирует повторный рендер полей при изменении значений, можно ли избежать избыточных обновлений.
Активность разработки — поддерживается ли проект, как часто выходят обновления, насколько активно сообщество.
Управление состоянием — какие подходы используются для хранения данных формы (локальный state, reducer, Redux и т.п.).
Валидация — какие механизмы встроены (синхронная, асинхронная, кастомные правила), насколько просто их подключать.
Для сравнения с Steroids Form мы выбрали несколько популярных библиотек для работы с формами:
Formik;
React Hook Form;
React Final Form.
Вот что у нас получилось:

React Hook Form и Formik по умолчанию не зависят от Redux — состояние форм в них хранится локально внутри компонентов. В отличие от них, Steroids Form изначально предоставляет встроенные механизмы синхронизации с Redux, сохраняя при этом возможность работать через React Reducer или локальный стейт, что делает библиотеку более гибкой в выборе хранилища данных.
React Hook Form минимизирует количество лишних перерисовок благодаря использованию неконтролируемых компонентов. В Formik при изменении данных зачастую перерисовывается вся форма, если специально не применять оптимизации. В Steroids, благодаря subscription-based архитектуре, количество перерисовок остаётся на среднем уровне.
Вывод
Steroids выгодно выделяется благодаря глубокой интеграции с React и Redux, что делает его особенно удобным для проектов, где критична работа с глобальным состоянием. При этом библиотека сохраняет хороший уровень производительности и гибкость за счёт поддержки разных способов хранения состояния. В экосистеме фреймворка Steroids это — оптимальный выбор.
