WebMriya
Назад до статей

React-форми без розростання стану

Практична архітектура React-форм, яка зберігає передбачуваними значення полів, валідацію, відправлення та помилки сервера.

React
TypeScript
Frontend Architecture
Software Architecture
W
Frontend Architecture & Legacy Modernization
9 жовтня 2026 р. · 6 хв читання

Форма починається невинно: три поля, одна кнопка відправлення і невеликий обробник onSubmit. Через шість місяців у ній уже є умовні секції, асинхронна валідація, помилки сервера, збереження чернетки, залежні поля та десяток булевих прапорців. Користувач бачить неправильне повідомлення про помилку, а інженер — компонент, який складно змінити без чергової регресії.

Проблема зазвичай не в самому React. Різні типи стану просто змішуються: що ввів користувач, чи валідне поле, чи виконується запит і що саме відхилив сервер. Якщо поводитися з усім цим як зі звичайним локальним станом компонента, форма стає складнішою для розуміння, ніж потрібно.

Спочатку розділіть стан, а вже потім обирайте інструменти

Надійна форма починається з інвентаризації стану. Кожне значення має мати одного власника і зрозумілу причину існування. У більшості production-форм є щонайменше чотири категорії:

  • Значення полів: поточна чернетка, наприклад email або вибраний тариф.
  • Стан взаємодії: чи було поле змінене, сфокусоване або втратило фокус.
  • Стан валідації: результати клієнтської валідації та повідомлення.
  • Стан відправлення: форма очікує запит, успішно завершила його або отримала помилку, зокрема помилки окремих полів від сервера.

Ці категорії можуть впливати одна на одну, але їх не варто зводити до одного об’єкта з випадковим набором прапорців. Поле не є невалідним лише тому, що користувач ще не втратив із нього фокус. Форма не є відправленою лише тому, що запит завершився помилкою. Явні відмінності між цими станами запобігають суперечливим комбінаціям.

Зберігайте чернетку як дані, а не як набір булевих прапорців

Чернетку зазвичай варто представляти типізованим об’єктом значень. Похідну інформацію, наприклад можливість натиснути кнопку відправлення, краще обчислювати з цього об’єкта, а не зберігати окремо. Інакше можна отримати ситуацію, коли `isValid: true`, хоча значення поля вже змінилося.

tsx
type SignupValues = {
  email: string;
  password: string;
  plan: "starter" | "team";
};

const initialValues: SignupValues = {
  email: "",
  password: "",
  plan: "starter"
};

const canSubmit =
  values.email.trim() !== "" &&
  values.password.length >=  twelve &&
  Object.keys(errors).length === 0;

Приклад показує форму даних, але головне правило — чітке володіння станом: `values` є єдиним джерелом правди для чернетки, а `errors` — джерелом правди для результатів валідації. У реальній реалізації потрібно виправити описку в порозі довжини та тримати правило валідації в одній повторно використовуваній функції, а не дублювати його в кнопці.

Для невеликих форм часто достатньо контрольованих інпутів і локального reducer. Для великих форм бібліотека може зменшити кількість повторюваного коду реєстрації полів і підписок. Але важливіше не конкретна бібліотека, а чіткі межі: значення не повинні дублюватися в незалежному стані, а валідація не має бути захована всередині презентаційних компонентів.

Валідуйте в правильні моменти

Валідація кожного поля після кожного натискання клавіші здається ретельною, але часто створює зайвий шум. Поле пароля може показати помилку, коли користувач ще ввів лише перший символ; залежне поле — помилку ще до вибору необхідної опції. Час валідації має відповідати взаємодії:

МоментКорисна поведінка
Початковий рендерНе показувати помилки, якщо це не форма редагування з уже невалідними даними.
Втрата фокусуВалідувати поле, яке користувач завершив переглядати.
Зміна після помилкиПовторно валідувати це поле, щоб повідомлення зникло одразу після виправлення.
ВідправленняПеревірити всі потрібні поля і сфокусувати першу помилку, яку можна виправити.
Відповідь сервераПовернути авторитетні помилки до відповідних полів, не перетираючи клієнтські повідомлення бездумно.

Тут особливо корисна schema-валідація. Одна схема може описувати обов’язкові поля, формати, діапазони та обмеження між полями. Перевірка на межі API важлива, тому що TypeScript не доводить коректність відповіді сервера або значення, яке контролює користувач. Детальніше про цю межу розповідає матеріал про runtime-валідацію.

Моделюйте відправлення як невелику машину станів

Логіка відправлення стає крихкою, коли описується незалежними прапорцями на кшталт `isSubmitting`, `hasSubmitted`, `isSuccess` та `submitFailed`. Деякі їхні комбінації неможливі, але код цього не забороняє. Discriminated union робить допустимі стани явними:

ts
type SubmitState =
  | { status: "idle" }
  | { status: "pending" }
  | { status: "success"; requestId: string }
  | { status: "error"; message: string; fieldErrors: Record<string, string> };

function getSubmitLabel(state: SubmitState) {
  if (state.status === "pending") return "Saving...";
  if (state.status === "success") return "Saved";
  return "Save changes";
}

Такий підхід працює і для складніших сценаріїв: початкова чернетка, збережена чернетка, збереження в процесі та невдале збереження — це різні стани з різними діями користувача. Саме тому явні стани інтерфейсу безпечніші за взаємодіючі булеві прапорці; дивіться матеріал про React state machines.

Вважайте помилки сервера окремим джерелом правди

Клієнтська валідація може перевірити, що email має правильну форму. Лише сервер знає, що адреса вже зареєстрована, вибраний тариф недоступний або користувач не має права редагувати запис. Не перетворюйте кожну серверну помилку на загальний toast. Помилки окремих полів потрібно показувати там, де користувач може їх виправити, а повідомлення форми залишати для проблем без корисної прив’язки до конкретного поля.

  • Використовуйте стабільні назви полів у форматі помилки API, щоб відповідь можна було зіставити без парсингу рядків.
  • Зберігайте загальну помилку форми для проблем авторизації, мережі та непередбачених збоїв.
  • Очищайте серверну помилку поля після його зміни або явно запускайте повторну валідацію.
  • Не вважайте, що успішний HTTP-запит означає успішне виконання бізнес-операції; перевіряйте тіло відповіді.

Уникайте непотрібних скидань і race condition

Після успішного відправлення не обов’язково автоматично скидати всю форму. У формі редагування це може стерти незбережені значення в іншій секції або несподівано перемістити фокус. Заздалегідь визначте, що потрібно продукту: очистити чернетку, замінити її відповіддю сервера чи залишити без змін, показавши підтвердження.

Асинхронна валідація та залежні опції створюють ще один ризик: старий запит може завершитися після нового й перезаписати правильний стан. Пов’язуйте відповіді зі значенням або запитом, який їх породив, скасовуйте застарілі запити, де це можливо, і ігноруйте відповіді, які більше не є актуальними. Це особливо важливо для перевірки імені користувача, пошуку адреси та select-компонентів із пошуком.

Практичний чекліст для рев’ю

  1. Визначте джерело правди для кожного значення поля, помилки та статусу відправлення.
  2. Приберіть зі стану значення, які можна обчислити з поточної чернетки.
  3. Опишіть, коли запускається кожне правило валідації та коли зникає його повідомлення.
  4. Представте взаємовиключні стани відправлення як discriminated union або стан reducer.
  5. Задокументуйте формат помилок API, включно з помилками полів і форми.
  6. Протестуйте втрату фокусу, виправлення після помилки, повторне відправлення, повільні та невдалі відповіді, а також застарілі асинхронні результати.

Форми — це невеликі workflow, а не просто колекції інпутів. Коли значення, валідація, взаємодія та відправлення змодельовані окремо, реалізацію легше тестувати, а інтерфейс стає передбачуванішим для користувача. У результаті з’являється не обов’язково більше абстракцій, а менше коду, який суперечить сам собі.

Коли форма є частиною складнішого продуктового сценарію, ті самі рішення впливають на маршрутизацію, права доступу, синхронізацію із сервером і відновлення після часткових збоїв. WebMriya допомагає командам створювати такі інтерфейси в межах складних вебзастосунків, зберігаючи архітектуру пропорційною реальним потребам продукту.

Поширені запитання

Чи має кожна React-форма використовувати form library?

Ні. Для невеликої форми контрольовані інпути та reducer можуть бути зрозумілішими. Бібліотека стає корисною, коли реєстрація полів, field arrays, підписки та підключення валідації створюють більше повторюваного коду, ніж команда готова підтримувати.

Чи потрібно запускати валідацію після кожного натискання клавіші?

Лише коли миттєвий зворотний зв’язок справді корисний, наприклад для вимог до пароля. Практичний варіант за замовчуванням — валідувати після втрати фокусу, а після появи помилки повторювати перевірку під час зміни поля та перевіряти всю форму під час відправлення.

Як представляти помилки окремих полів від сервера?

Використовуйте стабільну структуру поле-повідомлення разом із повідомленням форми для помилок, які не можна прив’язати до одного поля. Так інтерфейс показує дієвий зворотний зв’язок, не приховуючи проблеми авторизації чи мережі.