Форми у React Native 2026: React Hook Form + Zod, TextInput і валідація
Як зробити форми у React Native 2026: React Hook Form + Zod, Controller для TextInput, KeyboardAvoidingView, багатокрокові форми, асинхронна валідація та серверні помилки. З робочим кодом для iOS та Android.
Найкращий спосіб роботи з формами у React Native 2026 року, це поєднання React Hook Form v7.54+ для керування станом полів і Zod v3.24+ для схемної валідації. Така зв'язка мінімізує ре-рендери завдяки неконтрольованим інпутам, дає повноцінну типізацію TypeScript з коробки й переносить майже весь досвід із React Web у мобільний застосунок. Крім одного важливого шва: React Native TextInput не має нативної події ref-реєстрації, тому обов'язково потрібен Controller. Далі покажу код, який реально працює на iOS та Android з новою архітектурою.
React Hook Form 7.54+ у React Native працює через компонент Controller. Прямий register(), який ви знаєте з вебу, тут не спрацює через відсутність DOM refs.
Zod 3.24 у поєднанні з @hookform/resolvers/zod дає єдине джерело правди для типів і правил валідації. Тип форми виводиться через z.infer<typeof schema>.
Неконтрольована модель RHF помітно перевершує Formik та useState за продуктивністю на формах із 10+ полями, бо не викликає ре-рендерів усього дерева.
Для UX клавіатури обов'язково використовуйте KeyboardAvoidingView та keyboardShouldPersistTaps="handled" у ScrollView.
Багатокрокові форми зручно будувати через FormProvider + useFormContext, зберігаючи один useForm на всю послідовність кроків.
Асинхронну валідацію (перевірка унікальності email) реалізуйте через refine() у Zod або через опцію validate у полі rules.
Чому React Hook Form + Zod, а не Formik чи useState
Чесно кажучи, коли я перейшов з React Web у React Native, першим імпульсом було просто скопіювати підхід із useState для кожного поля. На простому екрані логіну це працює. На екрані реєстрації з 8 полями, паролем, підтвердженням пароля та інлайн-валідацією, вже ні. Кожне натискання клавіші тригерить ре-рендер батьківського компонента, всі поля перемальовуються, а на бюджетних Android-пристроях це відчувається як лаг клавіатури.
React Hook Form вирішує це через uncontrolled модель: значення полів живуть у внутрішньому сховищі бібліотеки, а не у React state батька. Ре-рендер відбувається тільки для полів, які реально змінилися, а решта дерева не оновлюється. За вимірюваннями команди RHF на React Native, форма на 20 полів робить у 4–6 разів менше ре-рендерів порівняно з Formik.
Zod же дає те, чого не було в Yup: справжній вивід TypeScript-типу зі схеми. Ви пишете схему один раз, і z.infer<typeof schema> автоматично дає вам тип форми, без жодних дублювань інтерфейсів. Це важливо для мобільних застосунків, де контракт із бекендом часто вужчий за UI-модель, і будь-яке розсинхронення дорівнює падінню в проді.
Якщо ви прийшли в React Native з вебу і вже знаєте RHF+Zod, добра новина: 90% API однакові. Погана: саме той шов, який відрізняється, легко пропустити, і про нього нижче.
Порівняльна таблиця бібліотек для форм
Перш ніж стрибати в код, ось конкретне порівняння трьох підходів, які зазвичай розглядають у React Native 2026. Я порівнюю саме ті виміри, які видно на практиці: розмір бандла, кількість ре-рендерів, підтримка TypeScript та комфорт роботи з валідацією.
Критерій
React Hook Form + Zod
Formik + Yup
useState
Розмір (min+gzip)
~9 КБ + 12 КБ
~13 КБ + 12 КБ
0 КБ
Модель полів
Неконтрольована через Controller
Контрольована
Контрольована
Ре-рендери на 20 полях
Тільки змінене поле
Уся форма
Уся форма
Вивід типів
z.infer, автоматично
Ручне дублювання
Ручне
Валідація
Zod-схема + resolvers
Yup-схема
Своя логіка
Крива навчання
Середня (Controller-паттерн)
Легка
Легка на старті, боляча в масштабі
Підтримка New Architecture
Так, з 0.76+
Так
Так
Активність у 2026
Активний розвиток
Мінімальна підтримка
Немає окремого пакета
Formik технічно ще працює, але останні великі релізи припинилися ще у 2023-му, а issue-трекер накопичив сотні відкритих багів. Для нових проєктів у 2026 році RHF+Zod фактично стандарт.
Встановлення та початкове налаштування
Створимо чистий Expo-проєкт і поставимо потрібний набір. Я припускаю React Native 0.76+ з новою архітектурою (Fabric+TurboModules), яка увімкнена за замовчуванням через Expo SDK 52+.
react-native ≥ 0.76 (для новоархітектурних оптимізацій TextInput)
Як використовувати React Hook Form з TextInput
Ось той самий шов, про який я згадав на початку. У React Web ви робите так:
// React Web, так НЕ буде в React Native
<input {...register("email")} />
register() покладається на нативні DOM refs та подію onChange у HTML-інпуті. У React Native TextInput це нативний компонент з іншим API: подія називається onChangeText, а ref не сумісний із web-DOM. Тому для кожного поля використовуємо Controller, який робить міст.
Зверніть увагу на дві дрібниці: onChangeText (не onChange!) і відсутність register(). Це той самий патерн, який використовує документація React Hook Form для React Native.
Валідація через Zod та @hookform/resolvers
Zod-схема, це і документація, і рантайм-перевірка, і TypeScript-тип в одному об'єкті. Розберемо типову схему реєстрації:
const registerSchema = z.object({
fullName: z.string().min(2, "Мінімум 2 символи").max(60),
email: z.string().email("Некоректний email"),
password: z
.string()
.min(8, "Мінімум 8 символів")
.regex(/[A-Z]/, "Хоча б одна велика літера")
.regex(/[0-9]/, "Хоча б одна цифра"),
confirmPassword: z.string(),
age: z.coerce.number().int().min(18, "Тільки для 18+"),
agree: z.literal(true, { errorMap: () => ({ message: "Погодьтесь з умовами" }) }),
}).refine((data) => data.password === data.confirmPassword, {
message: "Паролі не збігаються",
path: ["confirmPassword"],
});
type RegisterValues = z.infer<typeof registerSchema>;
Тут працюють кілька важливих речей:
z.coerce.number(): TextInput завжди повертає рядок, а coerce автоматично приводить його до числа перед валідацією.
refine(): перехресна перевірка двох полів (паролі), з явним указанням path, куди прикріпити помилку.
z.literal(true): чекбокс згоди з умовами; тип автоматично звузиться до true.
Далі підключаємо резолвер: useForm({ resolver: zodResolver(registerSchema) }). Прапорець mode: "onChange" вмикає інлайн-валідацію на кожен ввід, а mode: "onBlur" спрацьовує на втрату фокусу. Я зазвичай віддаю перевагу onBlur у мобільних, щоб не смикати користувача червоними написами.
Клавіатура, фокус та UX-шви React Native
Форми на мобільному, це передусім клавіатура, фокус і скрол. Ось три речі, які потрібно зробити завжди, інакше форма виглядатиме «як з дев'ятого поверху».
1. KeyboardAvoidingView
Обгортайте форму у KeyboardAvoidingView, інакше на iPhone клавіатура перекриє останнє поле:
import { KeyboardAvoidingView, Platform, ScrollView } from "react-native";
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<ScrollView keyboardShouldPersistTaps="handled">
{/* поля форми */}
</ScrollView>
</KeyboardAvoidingView>
keyboardShouldPersistTaps="handled", критично важлива опція. Без цього перший тап поза інпутом закриває клавіатуру, а не тригерить кнопку.
2. Перехід між полями через returnKeyType
Мобільні користувачі очікують кнопку «Next» на клавіатурі, яка стрибає у наступне поле:
Для полів пароля вказуйте textContentType="password" (iOS) та autoComplete="password" (Android), щоб працював менеджер паролів. Для одноразових кодів беріть textContentType="oneTimeCode", тоді iOS запропонує підставити SMS-код.
Багатокрокові форми та FormProvider
Класична пастка новачка: на кожному кроці wizard'а створювати окремий useForm(). Це втрачає стан між кроками, ускладнює валідацію в кінці і робить неможливим повернення назад із збереженням даних.
Правильний патерн такий: один useForm на корені майстра, всередині FormProvider, а екрани кроків читають контекст через useFormContext.
Метод trigger() перевіряє лише вказані поля, а не всю форму. Саме те, що потрібно між кроками. Це також дає керовану інтеграцію з Expo Router v6, якщо кроки реалізовані як окремі маршрути.
Асинхронна валідація та серверні помилки
Реальні форми рідко обмежуються синхронною валідацією. Треба перевіряти, чи email вже зайнятий, чи промокод діє, чи достатньо балансу. Є два підходи.
Через Zod refine
const schema = z.object({
email: z.string().email(),
}).superRefine(async (data, ctx) => {
const res = await fetch(`/api/check-email?email=${data.email}`);
const { taken } = await res.json();
if (taken) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["email"],
message: "Email вже зайнятий",
});
}
});
Через superRefine валідація виконується разом із синхронною, але тільки на submit. Не спамте бекенд на кожен натиск клавіші.
Через setError після відповіді сервера
Коли бекенд повертає помилку (наприклад, 422 з переліком полів), використовуйте setError:
const onSubmit = async (data: FormValues) => {
try {
await api.register(data);
} catch (err) {
if (err.status === 422) {
for (const field of err.fields) {
setError(field.name, { type: "server", message: field.message });
}
}
}
};
Для управління загальним станом застосунку після успішної відправки (токени, юзер-профіль) краще класти в окреме сховище, а не тримати у формі. Про підходи писав у гайді з управління станом у React Native 2026.
Типові помилки та як їх уникнути
За два роки в RN я бачив приблизно ті самі граблі у кожній команді, яка переходить із Formik або з голого useState. Ось топ-5.
Забутий defaultValues. Без нього поля стартують як undefined, RHF вважає їх «неконтрольованими», а Zod-схема з .min() не спрацює на порожньому полі, поки в нього щось не введуть.
Використання onChange замість onChangeText. Класичний web-рефлекс. Значення не оновлюється, і ви годинами шукаєте, чому форма «не бачить» введеного тексту.
Дублювання типів. Якщо ви пишете interface FormValues та схему Zod окремо, раніше чи пізніше вони розсинхроняться. Завжди type FormValues = z.infer<typeof schema>.
Валідація на onChange без debounce. Для важких схем (regex, refine) це помітно смикає UI. Або переходьте на onBlur, або обгортайте валідатор у debounce.
Забутий keyboardShouldPersistTaps. Кнопка «Відправити» не реагує на перший тап, коли клавіатура відкрита. Я витратив на це два години на своєму першому проєкті, і після цього завжди перевіряю опцію першою.
Чи можна використовувати React Hook Form у React Native без TypeScript?
Так, RHF повністю сумісний із чистим JavaScript. Але тоді ви втрачаєте головну перевагу зв'язки з Zod, тобто автоматичний вивід типу через z.infer. Валідація працює однаково, просто редактор не підказуватиме поля.
Що краще для React Native, React Hook Form чи Formik?
Для нових проєктів у 2026 році, React Hook Form. Formik досі працює, але майже не оновлюється з 2023 року, робить у 4–6 разів більше ре-рендерів на середніх формах і не має нативної підтримки Zod. RHF також менший за розміром бандла.
Як обробляти помилки з сервера у React Hook Form?
Використовуйте метод setError(fieldName, { type: "server", message }) у блоці catch вашої функції відправки. Помилка з'явиться поруч із полем так само, як клієнтські помилки Zod, і автоматично очиститься після наступного редагування.
Чому TextInput не оновлюється при введенні у React Hook Form?
Найчастіша причина, це використання onChange замість onChangeText у Controller. У React Native TextInput має власну подію onChangeText, яка передає рядок безпосередньо. Друга причина, відсутній defaultValues у useForm.
Чи можна робити асинхронну валідацію у Zod?
Так, через метод superRefine або звичайний refine, обидва підтримують асинхронні функції. Однак асинхронна перевірка виконується тільки при handleSubmit, а не на кожен натиск клавіші, тому не спамить бекенд.