Форми у React Native 2026: React Hook Form + Zod, TextInput і валідація

Як зробити форми у React Native 2026: React Hook Form + Zod, Controller для TextInput, KeyboardAvoidingView, багатокрокові форми, асинхронна валідація та серверні помилки. З робочим кодом для iOS та Android.

Форми React Native 2026: RHF + Zod Гайд

Оновлено: 8 липня 2026 року

Найкращий спосіб роботи з формами у 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 + ZodFormik + YupuseState
Розмір (min+gzip)~9 КБ + 12 КБ~13 КБ + 12 КБ0 КБ
Модель полівНеконтрольована через ControllerКонтрольованаКонтрольована
Ре-рендери на 20 поляхТільки змінене полеУся формаУся форма
Вивід типівz.infer, автоматичноРучне дублюванняРучне
ВалідаціяZod-схема + resolversYup-схемаСвоя логіка
Крива навчанняСередня (Controller-паттерн)ЛегкаЛегка на старті, боляча в масштабі
Підтримка New ArchitectureТак, з 0.76+ТакТак
Активність у 2026Активний розвитокМінімальна підтримкаНемає окремого пакета

Formik технічно ще працює, але останні великі релізи припинилися ще у 2023-му, а issue-трекер накопичив сотні відкритих багів. Для нових проєктів у 2026 році RHF+Zod фактично стандарт.

Встановлення та початкове налаштування

Створимо чистий Expo-проєкт і поставимо потрібний набір. Я припускаю React Native 0.76+ з новою архітектурою (Fabric+TurboModules), яка увімкнена за замовчуванням через Expo SDK 52+.

npx create-expo-app my-forms-app --template blank-typescript
cd my-forms-app
npm install react-hook-form zod @hookform/resolvers

Мінімальні вимоги до версій станом на 2026 рік:

  • react-hook-form ≥ 7.54.0
  • zod ≥ 3.24.0
  • @hookform/resolvers ≥ 3.9.0
  • 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, який робить міст.

import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { View, Text, TextInput, Button } from "react-native";

const schema = z.object({
  email: z.string().email("Некоректний email"),
  password: z.string().min(8, "Мінімум 8 символів"),
});

type FormValues = z.infer<typeof schema>;

export function LoginForm() {
  const { control, handleSubmit, formState: { errors } } = useForm<FormValues>({
    resolver: zodResolver(schema),
    defaultValues: { email: "", password: "" },
  });

  const onSubmit = (data: FormValues) => {
    // data типізовано як { email: string; password: string }
    console.log(data);
  };

  return (
    <View style={{ padding: 16 }}>
      <Controller
        control={control}
        name="email"
        render={({ field: { onChange, onBlur, value } }) => (
          <TextInput
            placeholder="Email"
            autoCapitalize="none"
            keyboardType="email-address"
            onBlur={onBlur}
            onChangeText={onChange}
            value={value}
          />
        )}
      />
      {errors.email && <Text style={{ color: "red" }}>{errors.email.message}</Text>}

      <Controller
        control={control}
        name="password"
        render={({ field: { onChange, onBlur, value } }) => (
          <TextInput
            placeholder="Пароль"
            secureTextEntry
            onBlur={onBlur}
            onChangeText={onChange}
            value={value}
          />
        )}
      />
      {errors.password && <Text style={{ color: "red" }}>{errors.password.message}</Text>}

      <Button title="Увійти" onPress={handleSubmit(onSubmit)} />
    </View>
  );
}

Зверніть увагу на дві дрібниці: 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» на клавіатурі, яка стрибає у наступне поле:

const passwordRef = useRef<RNTextInput>(null);

<TextInput
  returnKeyType="next"
  onSubmitEditing={() => passwordRef.current?.focus()}
  blurOnSubmit={false}
/>
<TextInput
  ref={passwordRef}
  returnKeyType="done"
  onSubmitEditing={handleSubmit(onSubmit)}
/>

3. Автоматичні атрибути iOS/Android

Для полів пароля вказуйте textContentType="password" (iOS) та autoComplete="password" (Android), щоб працював менеджер паролів. Для одноразових кодів беріть textContentType="oneTimeCode", тоді iOS запропонує підставити SMS-код.

Багатокрокові форми та FormProvider

Класична пастка новачка: на кожному кроці wizard'а створювати окремий useForm(). Це втрачає стан між кроками, ускладнює валідацію в кінці і робить неможливим повернення назад із збереженням даних.

Правильний патерн такий: один useForm на корені майстра, всередині FormProvider, а екрани кроків читають контекст через useFormContext.

import { FormProvider, useForm, useFormContext } from "react-hook-form";

function Wizard() {
  const methods = useForm<RegisterValues>({
    resolver: zodResolver(registerSchema),
    mode: "onBlur",
    defaultValues: { fullName: "", email: "", password: "", confirmPassword: "", agree: false, age: 18 },
  });
  const [step, setStep] = useState(0);

  return (
    <FormProvider {...methods}>
      {step === 0 && <StepPersonal onNext={() => setStep(1)} />}
      {step === 1 && <StepPassword onSubmit={methods.handleSubmit(onSubmit)} />}
    </FormProvider>
  );
}

function StepPersonal({ onNext }: { onNext: () => void }) {
  const { control, trigger } = useFormContext<RegisterValues>();
  const handleNext = async () => {
    const ok = await trigger(["fullName", "email"]); // валідує тільки поточні поля
    if (ok) onNext();
  };
  // ...
}

Метод 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.

  1. Забутий defaultValues. Без нього поля стартують як undefined, RHF вважає їх «неконтрольованими», а Zod-схема з .min() не спрацює на порожньому полі, поки в нього щось не введуть.
  2. Використання onChange замість onChangeText. Класичний web-рефлекс. Значення не оновлюється, і ви годинами шукаєте, чому форма «не бачить» введеного тексту.
  3. Дублювання типів. Якщо ви пишете interface FormValues та схему Zod окремо, раніше чи пізніше вони розсинхроняться. Завжди type FormValues = z.infer<typeof schema>.
  4. Валідація на onChange без debounce. Для важких схем (regex, refine) це помітно смикає UI. Або переходьте на onBlur, або обгортайте валідатор у debounce.
  5. Забутий keyboardShouldPersistTaps. Кнопка «Відправити» не реагує на перший тап, коли клавіатура відкрита. Я витратив на це два години на своєму першому проєкті, і після цього завжди перевіряю опцію першою.

Для профілювання ре-рендерів корисно вмикати React DevTools Highlight Updates. Детальніше про інструменти дивіться у гайді з зневадження React Native. Оригінальні API також є в офіційній документації React Native TextInput та офіційній документації Zod.

Часті запитання

Чи можна використовувати 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, а не на кожен натиск клавіші, тому не спамить бекенд.

Anita Iyer
Про Автора Anita Iyer

Cross-platform mobile developer who came to RN from web. Bridges the two worlds and explains the seams.