Version2

Форми, ввід і валідація

Усі основні контроли форми в React Native — TextInput, Pressable, Button, Switch, Checkbox, Slider, Picker, date picker; клавіатура; React Hook Form і Zod; реєстрація на подію та створення поїздки в Nomad

Вступ: особливості форм та вводу на мобільних пристроях

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

Мобільне введення даних підпорядковується базовим принципам людино-машинної взаємодії (Human-Computer Interaction, HCI):

  1. Закон Фіттса (Fitts's Law): час, необхідний для точного позиціонування пальця на елементі керування, є функцією відстані до цілі та її геометричного розміру (Завантаження...). Інтерфейсні елементи на сенсорному екрані повинні мати мінімальну фізичну площу детекції дотику (Touch Target Size) не менше 44×44 pt на iOS та 48×48 dp на Android.
  2. Перекриття клавіатурою (Keyboard Occlusion): поява віртуальної клавіатури (Soft Keyboard) миттєво скорочує корисну площу вікна перегляду на 40–55%, фізично перекриваючи елементи введення та кнопки завершення дії.
  3. Когнітивне навантаження та ціна помилки: відсутність тактильного відгуку фізичних клавіш суттєво підвищує ймовірність помилок друку (typos). Ефективна форма мінімізує відкритий текстовий ввід на користь вибору з готових списків, перемикачів і вибору дати.

Навчальні цілі розділу:

  • Розібрати основні нативні контроли введення React Native (TextInput, Switch, Checkbox, Slider, Picker, DateTimePicker) та навчитися обирати правильний елемент під конкретний тип даних.
  • Зрозуміти архітектурні відмінності між подієвою моделлю Web DOM та нативними віджетами операційної системи (iOS UITextField / Android EditText).
  • Зрозуміти, як працюють контрольовані компоненти (Controlled Components): двостороннє зв'язування, життєвий цикл синхронізації та усунення стану «залипання» (Input Freezing).
  • Налаштувати надійне запобігання перекриттю форми клавіатурою за допомогою зв'язки KeyboardAvoidingView + ScrollView + keyboardShouldPersistTaps="handled".
  • Побудувати надійну систему валідації на базі React Hook Form та схем Zod через адаптер @hookform/resolvers/zod з підтримкою режимів mode: 'onSubmit' та reValidateMode: 'onChange'.

Огляд та класифікація контролів введення

Форми у мобільних додатках складаються з різних елементів керування, кожен з яких оптимізований під конкретний тип даних:

+-------------------------------------------------------------------------+
|                  КЛАСИФІКАЦІЯ КОНТРОЛІВ REACT NATIVE                    |
+-------------------------------------------------------------------------+
|  Тип даних               | Нативний компонент   | Платформова реалізація|
+--------------------------+----------------------+-----------------------+
|  Вільний текст           | TextInput            | UITextField / EditText|
|  Бінарний перемикач      | Switch               | UISwitch / Switch     |
|  Логічна згода           | Checkbox (Community) | Checkbox / Custom SVG |
|  Неперервний діапазон    | Slider (Community)   | UISlider / SeekBar    |
|  Дискретний список (1..N)| Picker (Community)   | UIPicker / Spinner    |
|  Календарна дата / час   | DateTimePicker (Comm)| UIDatePicker / Dialog |
+-------------------------------------------------------------------------+

Порівняльна матриця вибору контролів

Тип данихРекомендований контрольАльтернатива (Антипатерн)Чому це критично для UX
Довільний текст (Email, Ім'я)TextInputСпроба створити псевдо-інпут на PressableДоступ до системної клавіатури, буфера обміну, автозаповнення та предиктивного тексту.
Бінарне налаштування (Так/Ні)SwitchТекстове поле «напишіть так/ні»Миттєва зміна стану одним рухом без виклику клавіатури.
Юридична згода (GDPR)CheckboxSwitchЧекбокс асоціюється з підписанням умов договору; вимагає явної фіксації згоди.
Орієнтовне число (1–14 днів)SliderВільний числовий TextInputЗапобігає введенню від'ємних чисел, літер та дробових значень; наочно візуалізує масштаб.
Фіксований набір (3–10 опцій)PickerТекстове поле для назви регіонуПовністю виключає одруківки, різні регістри та невалідні значення в базі даних.
Календарна датаDateTimePickerТекстове поле «ДД.ММ.РРРР»Захищає від неіснуючих дат (наприклад, 31 лютого), вирішує конфлікти локальних форматів.

Архітектурне порівняння: Web React проти React Native

Розробники з досвідом у веб-розробці часто припускаються системних помилок, переносячи браузерні ментальні моделі в мобільне середовище.

       ВЕБ-АРХІТЕКТУРА (DOM)                     REACT NATIVE (NATIVE VIEWS)
+---------------------------------+       +---------------------------------------+
|  <form onSubmit={...}>          |       |  <View> (Контейнер без форми)         |
|    <input type="email" />       |  vs   |    <TextInput keyboardType="email" /> |
|    <button type="submit" />     |       |    <Pressable onPress={handleSubmit}> |
|  </form>                        |       |  </View>                              |
+---------------------------------+       +---------------------------------------+
| • Події спливають (Bubbling)    |       | • Немає спливання подій               |
| • Синтетичний event.target.value|       | • onChangeText передає чистий рядок   |
| • Неконтрольовані ref у RHF     |       | • RHF вимагає Controller              |
+---------------------------------+       +---------------------------------------+

1. Текстовий ввід та модель подій

  • Web: елементи <input> та <textarea> породжують синтетичні події onChange, де нове значення витягується через event.target.value. Спеціалізація поля задається єдиним атрибутом type (text, password, email, number), який одночасно керує валідацією, відображенням та поведінкою браузера.
  • React Native: компонент TextInput не є DOM-вузлом. Це декларативна обгортка над нативним UITextField в iOS або EditText в Android. Колбек onChangeText одразу передає готовий нормалізований рядок (text: string) => void. Атрибут type відсутній — поведінка налаштовується окремими параметрами: keyboardType (тип клавіатури), secureTextEntry (маскування), multiline (багаторядковість), autoCapitalize та autoCorrect.

2. Відсутність контейнера <form> та механізму Submit

  • Web: тег <form> реалізує вбудовану подію submit, яка автоматично перехоплює натискання клавіші Enter на будь-якому вкладеному інпуті та ініціює валідацію або надсилання даних.
  • React Native: концепція HTML-форми відсутня. Немає автоматичного спливання подій (Event Bubbling) до спільного батьківського контейнера. Запуск валідації та відправка форми завжди є явною процедурою: виклик функції handleSubmit у властивості onPress кастомної кнопки Pressable.

3. Екосистема компонентів: Core проти Community Packages

У браузері всі базові контроли (<select>, <input type="checkbox">, <input type="range">, <input type="date">) вбудовані в HTML5-стандарт.

У React Native базове ядро навмисно полегшене (Lean Core initiative):

  • У ядрі react-native збережено лише TextInput та Switch.
  • Контроли Picker, Slider, Checkbox, DateTimePicker винесені в офіційно підтримувані пакети спільноти (Community Packages): @react-native-picker/picker, @react-native-community/slider, @react-native-community/datetimepicker, @react-native-community/checkbox.
  • Встановлення таких модулів здійснюється строго через менеджер пакетів Expo: npx expo install <package-name>, що гарантує сумісність нативних бінарних залежностей із поточною версією Expo SDK.

4. Прив'язка полів у React Hook Form: register проти Controller

У веб-додатках бібліотека React Hook Form спирається на неконтрольовані DOM-інпути через прямий доступ до рефів: <input {...register('email')} />.

У React Native прямий доступ до DOM-рефів неможливий через ізоляцію нативних шарів. Єдиним архітектурно коректним способом інтеграції є компонент Controller, який транслює внутрішній стан RHF у явні пропси контрольованого компонента (value, onChange, onBlur).


Теорія контрольованих значень (Controlled Components)

У декларативній моделі React Native базовим патерном роботи з формами є контрольований компонент (Controlled Component).

Контрольованим називається компонент, відображення якого повністю визначається станом React (Single Source of Truth), а будь-яка спроба користувача змінити цей стан ініціює подію зворотного виклику:

Завантаження...
\text{State } S \xrightarrow{\quad\text{render}\quad} \text{UI View } (value = S) \xrightarrow{\quad\text{user touch}\quad} \text{Event } (onChangeText(S')) \xrightarrow{\quad\text{setState}\quad} S'

Універсальна матриця зв'язування контролів

Кожен контроль у React Native реалізує цей двосторонній контракт через відповідну пару пропсів:

КонтролТип значення (State Type)Проп передачі значення (Value Prop)Проп підписки на зміну (Change Event Prop)
TextInputstringvalue={text}onChangeText={(text) => setText(text)}
Switchbooleanvalue={isEnabled}onValueChange={(bool) => setIsEnabled(bool)}
Checkboxbooleanvalue={isChecked}onValueChange={(bool) => setIsChecked(bool)}
Slidernumbervalue={num}onValueChange={(num) => setNum(num)}
PickerT (string | number)selectedValue={val}onValueChange={(val) => setVal(val)}
DateTimePickerDatevalue={date}onChange={(event, date) => setDate(date)}

Наслідки порушення контракту контрольованості:

Якщо розробник передає проп значення (value), але забуває оновити стан у колбеці (onChangeText / onValueChange), інтерфейс впадає у стан «залипання» (Input Freezing). Нативний віджет отримує новий символ від клавіатури, повідомляє React про зміну, але під час фази узгодження (Reconciliation) React примусово відновлює старе значення value зі стану. Користувач сприймає це як фатальний баг або апаратне зависання екрана.


TextInput: поглиблений аналіз нативного текстового поля

Компонент TextInput є прямим містком до нативних компонентів текстового введення операційної системи:

  • iOS: інкапсулює нативний клас UITextField (для однорядкового вводу) або UITextView (для багаторядкового режиму multiline={true}).
  • Android: транслюється в нативний віджет android.widget.EditText, інтегрований із платформовим менеджером редакторів методу введення (Input Method Editor, IME).

На відміну від статичних графічних елементів, TextInput підтримує складний системний стан: поточну позицію каретки (cursor position), виділений діапазон символів (selection range), чергу асинхронних подій автокорекції, буфер проміжних символів клавіатури (для ієрогліфічного чи предиктивного вводу) та активне віконне з'єднання з процесом клавіатури.

Життєвий цикл контрольованого введення

import { useState } from 'react';
import { StyleSheet, Text, TextInput, View } from 'react-native';

export function NameField() {
  const [name, setName] = useState('');

  return (
    <View style={styles.container}>
      <Text style={styles.label}>Повне ім'я</Text>
      <TextInput
        value={name}
        onChangeText={setName}
        placeholder="Олена Ковальчук"
        autoCapitalize="words"
        autoCorrect={false}
        style={styles.input}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: { gap: 6 },
  label: { fontSize: 13, fontWeight: '700', textTransform: 'uppercase' },
  input: {
    borderWidth: 1,
    borderRadius: 12,
    paddingHorizontal: 14,
    paddingVertical: 12,
    fontSize: 16,
    minHeight: 48,
  },
});

Покроковий механізм обробки вводу:

  1. Користувач натискає клавішу на системній клавіатурі.
  2. Нативний віджет (UITextField / EditText) фіксує символ та ініціює подію через міст/JSI до JavaScript-потоку.
  3. Викликається обробник onChangeText(newString) із повним новим рядком.
  4. Функція setName планує оновлення стану React.
  5. Фаза Reconciliation обчислює новий VDOM і передає оновлене значення value назад у нативний віджет для синхронізації відображення.

Повний огляд параметрів (Props) TextInput

value
string
Поточний текстовий вміст поля. Передача цього параметра переводить TextInput у строго контрольований режим (Controlled Mode). Компонент гарантує відповідність тексту на екрані значенню у вашому стані React / React Hook Form. Якщо стан не оновлюється під час виклику onChangeText, компонент примусово відновлює попереднє значення зі стану.
onChangeText
(text: string) => void
Основний обробник зміни вмісту. На відміну від веб-стандарту onChange (який повертає складний об'єкт SyntheticEvent), onChangeText одразу передає десеріалізований нормалізований рядок text. Викликається на кожен введений, видалений або вставлений з буфера символ.
defaultValue
string
Початкове значення для неконтрольованого режиму (Uncontrolled Mode). Нативний віджет самостійно керує текстовим буфером без синхронізації з деревом станів React. Використовується рідко, оскільки ускладнює програмне скидання форми та валідацію схем.
placeholder / placeholderTextColor
string / ColorValue
Текст підказки, що відображається всередині поля за умови відсутності введених символів (value === ''). Зникає в момент введення першого символу. Обов'язково налаштовуйте placeholderTextColor відповідно до активної кольорової схеми (Light/Dark), щоб уникнути втрати контрастності.
editable
boolean (дефолт: true)
Визначає інтерактивність поля. При значенні false поле переходить у режим «лише для читання» (Read-Only): текст залишається видимим, але тапи ігноруються, а клавіатура не з'являється. Застосовується під час виконання асинхронного submit-запиту для блокування повторного редагування.
multiline
boolean (дефолт: false)
Перемикає компонент у багаторядковий режим. На iOS перемикає базовий віджет на UITextView. На платформі Android для забезпечення правильного вирівнювання тексту за верхнім краєм контейнера обов'язково додавайте властивість textAlignVertical: 'top' у стилях.
maxLength
number
Жорстке апаратне обмеження максимальної кількості символів на рівні нативного віджета. Запобігає введенню зайвих символів безпосередньо в момент друку. Має дублюватися правилами схеми Zod (.max(N)).
secureTextEntry
boolean (дефолт: false)
Активує режим безпечного введення (маскування символів крапками/зірочками) для паролів та конфіденційних токенів. Працює виключно в однорядковому режимі (multiline={false}).
keyboardType
KeyboardTypeOptions
Визначає розкладку та конфігурацію клавіатури, що надається операційною системою:
  • default — стандартна алфавітно-цифрова клавіатура.
  • email-address — розкладка з оптимізованим доступом до символу @, крапки та доменних зон.
  • numeric / number-pad — цифровий блок для введення чисел, PIN-кодів та сум.
  • phone-pad — телефонна клавіатура з символами +, *, #.
  • url — оптимізація для введення веб-посилань (/, .com).
  • decimal-pad — цифрова клавіатура з підтримкою десяткового роздільника (крапка/кома відповідно до регіональних налаштувань ОС).
autoCapitalize
'none' | 'sentences' | 'words' | 'characters'
Керує системною автоматичною капіталізацією (переведенням перших літер у верхній регістр):
  • none — обов'язковий вибір для email, паролів, URL та логінів.
  • sentences — заголовні літери на початку кожного речення (стандарт для нотаток).
  • words — заголовні літери для кожного слова (імена людей, назви міст).
  • characters — усі літери вводяться у верхньому регістрі (серійні номери, промокоди, IBAN).
autoCorrect
boolean (дефолт: true)
Вмикає або вимикає системний словник автовиправлення. Для технічних полів (Email, адреси гаманців, коди) обов'язково встановлюйте autoCorrect={false}.
returnKeyType
ReturnKeyTypeOptions
Конфігурує підпис та функціональне призначення клавіші підтвердження на віртуальній клавіатурі (next, done, go, search, send). Допомагає користувачеві орієнтуватися в ланцюжку полів.
onSubmitEditing
() => void
Колбек, що викликається при натисканні клавіші Return на клавіатурі. Базовий інструмент для реалізації переходу фокусу на наступне поле форми через імперативний виклик nextInputRef.current?.focus().
blurOnSubmit
boolean
Визначає, чи слід знімати фокус із поточного поля та закривати клавіатуру після натискання клавіші Return. Для проміжних полів форми встановлюють blurOnSubmit={false} у поєднанні з returnKeyType="next".
onFocus / onBlur
() => void
Події отримання та втрати полем віконного фокусу. Використовуються для підсвічування рамок активного інпуту, а також бібліотекою React Hook Form у режимах валідації mode: 'onBlur' або mode: 'onTouched'.
selection
{ start: number, end?: number }
Імперативне або контрольоване управління позицією курсору та виділеним фрагментом тексту.

Програмне управління фокусом та ланцюжки введення

На мобільних пристроях перехід між полями форми здійснюється за допомогою імперативних рефів useRef<TextInput>(null). Це усуває необхідність закривати клавіатуру та вручну тапати по наступному інпуту:

import { useRef } from 'react';
import { TextInput, View } from 'react-native';

export function ChainedInputsForm() {
  const emailRef = useRef<TextInput>(null);
  const passwordRef = useRef<TextInput>(null);

  return (
    <View>
      <TextInput
        placeholder="Ім'я користувача"
        returnKeyType="next"
        blurOnSubmit={false}
        onSubmitEditing={() => emailRef.current?.focus()}
      />
      <TextInput
        ref={emailRef}
        placeholder="Електронна пошта"
        keyboardType="email-address"
        autoCapitalize="none"
        returnKeyType="next"
        blurOnSubmit={false}
        onSubmitEditing={() => passwordRef.current?.focus()}
      />
      <TextInput
        ref={passwordRef}
        placeholder="Пароль"
        secureTextEntry
        returnKeyType="done"
        onSubmitEditing={() => console.log('Submit')}
      />
    </View>
  );
}

Демо: різні типи клавіатур

TSXTextInputTypes.tsx
iPhone
9:41

Loading…

react-native-web · not a real device


Pressable та Button: механіка ініціації дій та стани взаємодії

Поля форми відповідають за збір та десеріалізацію даних, тоді як кнопки фіксують намір користувача (User Intent): підтвердження, скидання, перехід до наступного кроку або скасування.

Архітектура Pressable: скінченний автомат обробки дотиків

Базовий компонент Pressable є низькорівневим інструментом відстеження жестової взаємодії, побудованим за принципом скінченного автомата станів (Finite State Machine):

       +---------------------------------------------+
       |                  IDLE                       |
       +---------------------------------------------+
           |                                     ^
       onPressIn                             onPressOut / Cancel
           v                                     |
       +---------------------------------------------+
       |   PRESSED (style: ({ pressed }) => [...])   |
       +---------------------------------------------+
           |
       Touch Released (within hitSlop boundary)
           v
       +---------------------------------------------+
       |          EXECUTE: onPress (Submit)          |
       +---------------------------------------------+

Ключові можливості та ергономіка Pressable:

  1. Динамічна функція стилів (style={({ pressed }) => [...]}): Надає прямий доступ до стану натискання в момент контакту пальця з екраном. Це дозволяє реалізувати миттєвий візуальний зворотний зв'язок (зміна непрозорості opacity: 0.85 або трансформація масштабу transform: [{ scale: 0.98 }]) до запуску важких асинхронних операцій.
  2. Закон Фіттса та hitSlop: Властивість hitSlop: Insets ({ top: 12, bottom: 12, left: 12, right: 12 }) розширює фізичну площу детекції дотику за межі візуальних рамок елемента без порушення геометричного розташування сусідніх компонентів у Flexbox. Це гарантує відповідність нормам доступності (мінімум 44×44 pt).
  3. Захист від подвійного сабміту (Idempotency & Race Conditions): При асинхронній відправці форми обов'язково використовується стан блокування disabled={isSubmitting}:
import { ActivityIndicator, Pressable, StyleSheet, Text } from 'react-native';

interface SubmitButtonProps {
  onPress: () => void;
  isSubmitting: boolean;
  title: string;
}

export function SubmitButton({ onPress, isSubmitting, title }: SubmitButtonProps) {
  return (
    <Pressable
      onPress={onPress}
      disabled={isSubmitting}
      style={({ pressed }) => [
        styles.button,
        pressed && !isSubmitting && styles.buttonPressed,
        isSubmitting && styles.buttonDisabled,
      ]}
    >
      {isSubmitting ? (
        <ActivityIndicator color="#FFFFFF" />
      ) : (
        <Text style={styles.buttonText}>{title}</Text>
      )}
    </Pressable>
  );
}

const styles = StyleSheet.create({
  button: {
    backgroundColor: '#2563EB',
    paddingVertical: 14,
    borderRadius: 12,
    alignItems: 'center',
    justifyContent: 'center',
    minHeight: 48,
  },
  buttonPressed: { opacity: 0.85, transform: [{ scale: 0.99 }] },
  buttonDisabled: { backgroundColor: '#94A3B8' },
  buttonText: { color: '#FFFFFF', fontSize: 16, fontWeight: '700' },
});

Системний Button проти кастомного Pressable

ХарактеристикаСистемний Button (react-native)Кастомний Pressable (Дизайн-система)
РендерингНативний віджет платформи (iOS Button / Android Material Button)Повна композиція React Native елементів
СтилізаціяОбмежена (лише колір тексту color на iOS або фон на Android)Необмежена (градієнти, тіні, скруглення, анімації)
Вкладений контентТільки статичний рядок titleБудь-яке дерево компонентів (Text, Icon, ActivityIndicator)
ЗастосуванняШвидке прототипування, системні діалогиПродакшн-інтерфейси, уніфіковані дизайн-системи
TSXFormPressable.tsx
iPhone
9:41

Loading…

react-native-web · not a real device


Switch: бінарні стани та платформні перемикачі

Компонент Switch є базовим елементом керування для бінарних значень (true / false), який транслюється в нативний віджет UISwitch на iOS та android.widget.Switch (або SwitchCompat) на платформі Android.

Ергономічна семантика: Switch проти Checkbox

КритерійSwitch (Тумблер)Checkbox (Прапорець)
UX-метафораФізичний тумблер живлення (увімкнено / вимкнено).Позначення пункту у друкованому бланку або списку умов.
Характер діїМиттєва дія або глобальне налаштування (темна тема, приватність, Bluetooth).Декларативна згода або множинний вибір із переліку опцій.
Швидкість сприйняттяСтан зчитується за просторовим положенням повзунка (thumb) та колірною підсвіткою.Стан зчитується за наявністю або відсутністю символу галочки всередині контуру.

Базовий контрольований приклад

import { useState } from 'react';
import { StyleSheet, Switch, Text, View } from 'react-native';

export function PrivateToggle() {
  const [isPrivate, setIsPrivate] = useState(false);

  return (
    <View style={styles.container}>
      <View style={styles.textGroup}>
        <Text style={styles.title}>Приватний доступ</Text>
        <Text style={styles.subtitle}>Приховати маршрут із публічного каталогу</Text>
      </View>
      <Switch
        value={isPrivate}
        onValueChange={setIsPrivate}
        trackColor={{ false: '#E2E8F0', true: '#2563EB' }}
        thumbColor="#FFFFFF"
        ios_backgroundColor="#E2E8F0"
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    paddingVertical: 12,
  },
  textGroup: { flex: 1, paddingRight: 16 },
  title: { fontSize: 16, fontWeight: '600' },
  subtitle: { fontSize: 13, color: '#64748B', marginTop: 2 },
});

Повний огляд параметрів (Props) Switch

value
boolean
Поточний бінарний стан перемикача. true відповідає активному положенню (ON), false — пасивному (OFF). У строго контрольованому режимі вимагає обов'язкової синхронізації зі станом React або контролером RHF.
onValueChange
(value: boolean) => void
Функція зворотного виклику, яка спрацьовує в момент завершення жесту перемикання або тапу. Передає єдиний аргумент нового булевого стану (nextValue: boolean).
disabled
boolean (дефолт: false)
Блокує інтерактивність компонента. Візуально нативний віджет переходить у напівпрозорий приглушений стан, ігноруючи будь-які жести користувача.
trackColor
{ false?: ColorValue, true?: ColorValue }
Об'єкт конфігурації кольору підкладки (доріжки) для неактивного (false) та активного (true) станів. Дозволяє узгодити системний перемикач із дизайн-токенами застосунку.
thumbColor
ColorValue
Колір рухомого кругового маніпулятора (thumb). На платформі Android запобігає візуальному злиттю повзунка з підкладкою при використанні кастомних фонів.
ios_backgroundColor
ColorValue
Спеціалізований проп для платформи iOS, що визначає фоновий колір контейнера навколо перемикача в неактивному стані (value === false).

Демо

TSXSwitchDemo.tsx
iPhone
9:41

Loading…

react-native-web · not a real device


Checkbox: прапорці підтвердження та юридична валідація

Checkbox (прапорець вибору) — спеціалізований елемент графічного інтерфейсу для фіксації підтвердження згоди з правилами (Terms of Service), вибору опцій у багатоваріантних анкетах або активації додаткових параметрів.

Оскільки у ядрі react-native відсутній кросплатформовий чекбокс, стандартом екосистеми є community-модуль @react-native-community/checkbox.

Встановлення залежності

npx expo install @react-native-community/checkbox

Базовий контрольований приклад

import { useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import Checkbox from '@react-native-community/checkbox';

export function TermsAgreement() {
  const [accepted, setAccepted] = useState(false);

  return (
    <View style={styles.row}>
      <Checkbox
        value={accepted}
        onValueChange={setAccepted}
        boxType="square"
        tintColors={{ true: '#2563EB', false: '#CBD5E1' }}
      />
      <Text style={styles.label}>
        Я погоджуюся з регламентом обробки персональних даних
      </Text>
    </View>
  );
}

const styles = StyleSheet.create({
  row: { flexDirection: 'row', alignItems: 'center', gap: 12, paddingVertical: 8 },
  label: { flex: 1, fontSize: 14, color: '#334155', lineHeight: 20 },
});

Повний огляд параметрів (Props) Checkbox

value
boolean
Поточний логічний стан прапорця. true відображає встановлену позначку (галочку), false — порожній контур.
onValueChange
(value: boolean) => void
Колбек події зміни стану, який викликається при тапі по елементу. Передає десеріалізоване значення нового булевого стану.
disabled
boolean (дефолт: false)
Блокує взаємодію з чекбоксом. Застосовується для обов'язкових умов, які розблоковуються лише після ознайомлення з текстом угоди.
tintColors
{ true?: ColorValue, false?: ColorValue } (Android)
Налаштування колірної гами для платформи Android: активний акцентний колір позначки (true) та колір контуру в пасивному стані (false).
onCheckColor / onFillColor / onTintColor
ColorValue (iOS)
Низькорівневе налаштування графіки на iOS: колір самої галочки (onCheckColor), внутрішня заливка активного квадрата (onFillColor) та колір рамки (onTintColor).
boxType
'circle' | 'square' (iOS)
Геометрична форма контуру прапорця на iOS. Для юридичних угод та згод традиційно використовується квадратна форма ('square').

Валідація обов'язкової згоди у Zod: z.boolean() проти z.literal(true)

У формах реєстрації та транзакцій існує принципова різниця між опційним прапорцем та обов'язковою юридичною згодою:

import { z } from 'zod';

export const registrationSchema = z.object({
  // 1. Опційне булеве поле: приймає true або false
  subscribeToNewsletter: z.boolean(),

  // 2. Обов'язкова згода: валідація проходить ТІЛЬКИ якщо значення строго дорівнює true
  acceptedTerms: z.literal(true, {
    errorMap: () => ({ message: 'Для продовження необхідно підтвердити згоду з правилами' }),
  }),
});

При використанні z.literal(true) початковим значенням у defaultValues завжди має бути false. Це гарантує свідому дію користувача (Explicit User Action) відповідно до стандартів GDPR та безпеки інтерфейсів.


Slider: маніпуляція числовими діапазонами

Slider (повзунок) — елемент керування для вибору скалярного числового значення в замкненому інтервалі Завантаження... за допомогою неперервного або квантованого жестового перетягування.

У сучасних версіях React Native використовується офіційний пакет спільноти @react-native-community/slider.

Встановлення залежності

npx expo install @react-native-community/slider

Коли варто використовувати Slider

  • Оптимальні сценарії: емпіричні величини, де користувач оперує відчуттям масштабу, а не точним рядковим значенням (тривалість подорожі 1–14 днів, радіус пошуку 5–50 км, рівень гучності, яскравість).
  • Недоцільні сценарії: дискретні точні дані (вік, грошові суми для транзакцій, номери телефонів), де похибка позиціонування пальця створює високе когнітивне навантаження.

Базовий контрольований приклад

import { useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import Slider from '@react-native-community/slider';

export function DaysDurationSelector() {
  const [days, setDays] = useState(3);

  return (
    <View style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.label}>Орієнтовна тривалість</Text>
        <Text style={styles.valueText}>{days} {days === 1 ? 'день' : 'днів'}</Text>
      </View>
      <Slider
        minimumValue={1}
        maximumValue={14}
        step={1}
        value={days}
        onValueChange={setDays}
        minimumTrackTintColor="#2563EB"
        maximumTrackTintColor="#CBD5E1"
        thumbTintColor="#2563EB"
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: { paddingVertical: 8, gap: 8 },
  header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
  label: { fontSize: 14, fontWeight: '600', color: '#334155' },
  valueText: { fontSize: 15, fontWeight: '700', color: '#2563EB' },
});

Повний огляд параметрів (Props) Slider

value
number
Поточне числове значення повзунка. Має задовольняти нерівність Завантаження.... Сам компонент не відображає текстовий лейбл значення, тому його виведення реалізується через суміжний елемент <Text>.
onValueChange
(value: number) => void
Високочастотний колбек, що викликається в процесі перетягування повзунка на кожну зміну координати з урахуванням параметра step. Ідеальний для синхронізації локального UI-підпису та стану форми React Hook Form.
onSlidingComplete
(value: number) => void
Колбек, який гарантовано спрацьовує лише один раз у момент завершення фізичного дотику (Touch Up). Застосовується для ресурсомістких операцій (мережеві запити, складні обчислення, аналітика), щоб не перевантажувати JS-потік під час жесту.
minimumValue / maximumValue
number (дефолт: 0 / 1)
Нижня та верхня межі діапазону. Завжди дублюються в схемі валідації Zod: z.number().min(min).max(max).
step
number (дефолт: 0)
Крок дискретизації. step={1} обмежує значення цілими числами. При step={0} значення є неперервним числом із плаваючою крапкою (Float).
minimumTrackTintColor / maximumTrackTintColor / thumbTintColor
ColorValue
Колірні дескриптори: активна ліва частина доріжки (minimumTrackTintColor), пасивна права частина (maximumTrackTintColor) та маніпулятор (thumbTintColor).
disabled
boolean (дефолт: false)
Блокує можливість перетягування повзунка.

Picker: вибір із фіксованого списку варіантів

Компонент Picker вирішує задачу селекції одного дискретного елемента з кінцевого переліку доступних варіантів.

У веб-розробці цьому відповідає селектор <select>. У React Native ця функціональність реалізована через незалежний модуль спільноти @react-native-picker/picker.

Встановлення залежності

npx expo install @react-native-picker/picker

Селекційна дилема: Picker проти вільного TextInput

  • Picker доречний: для фіксованих списків від 3 до 15 елементів (регіони країни, мовні локалі, категорії витрат, валюти, ролі доступу). Усуває помилки друку (typos) та гарантує цілісність даних на клієнті й сервері.
  • TextInput доречний: для нескінченних або відкритих наборів (пошукові запити, назви міст, описи).
  • Окремий навігаційний екран з FlatList та пошуком: найкращий патерн, коли кількість варіантів перевищує 20 або вимагає фільтрації (наприклад, вибір аеропорту або країни).

Базовий контрольований приклад

import { useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Picker } from '@react-native-picker/picker';

export const TRIP_REGIONS = [
  'Карпати',
  'Поділля',
  'Полісся',
  'Причорноморʼя',
  'Слобожанщина',
] as const;

export type TripRegion = (typeof TRIP_REGIONS)[number];

export function RegionSelector() {
  const [region, setRegion] = useState<TripRegion>('Карпати');

  return (
    <View style={styles.container}>
      <Text style={styles.label}>Географічний регіон</Text>
      <View style={styles.pickerWrapper}>
        <Picker
          selectedValue={region}
          onValueChange={(itemValue) => setRegion(itemValue as TripRegion)}
        >
          {TRIP_REGIONS.map((item) => (
            <Picker.Item key={item} label={item} value={item} />
          ))}
        </Picker>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { gap: 6 },
  label: { fontSize: 13, fontWeight: '700', textTransform: 'uppercase', color: '#64748B' },
  pickerWrapper: {
    borderWidth: 1,
    borderColor: '#E2E8F0',
    borderRadius: 12,
    overflow: 'hidden',
  },
});

Платформова дивергенція відображення

На операційній системі iOS Picker за замовчуванням візуалізується як вертикальний барабан (Wheel / UIPickerView). Він займає фіксовану значну висоту (близько 216 pt). Вбудовування такого колеса безпосередньо у форму може порушувати вертикальну композицію, тому в професійних мобільних інтерфейсах (зокрема в проєкті Nomad) його часто розміщують у висувному модальному вікні (Bottom Sheet) або активують по кліку на статичний селектор-рядок.

Синхронізація схеми Zod та TypeScript-типів (Single Source of Truth)

Щоб уникнути розбіжності між інтерфейсом та валідатором, масив констант типізується через as const:

import { z } from 'zod';

export const TRIP_REGIONS = ['Карпати', 'Поділля', 'Полісся', 'Причорноморʼя'] as const;

export const tripSchema = z.object({
  region: z.enum(TRIP_REGIONS, {
    errorMap: () => ({ message: 'Оберіть дійсний регіон зі списку' }),
  }),
});

export type TripFormValues = z.infer<typeof tripSchema>;

DateTimePicker: темпоральний ввід та системні календарі

Введення календарних дат та часових міток через вільний TextInput є грубим антипатерном мобільного UX через конфлікт локальних форматів (американський MM/DD/YYYY проти європейського DD.MM.YYYY), складність ручної валідації високосних років і зміщення часових поясів (Timezone Offsets).

Стандартом React Native є пакет @react-native-community/datetimepicker.

Встановлення залежності

npx expo install @react-native-community/datetimepicker

Платформові відмінності життєвого циклу подій

На Android виклик <DateTimePicker /> відкриває асинхронний системний модальний діалог DatePickerDialog. Колбек onChange передає об'єкт події DateTimePickerEvent, де поле event.type визначає результат:

  • event.type === 'set' — користувач підтвердив дату; нове значення міститься в selectedDate.
  • event.type === 'dismissed' — користувач закрив діалог клавішею Back або кнопкою «Скасувати».

Розробник зобов'язаний одразу перевести прапорець показу діалогу в false:

const onDateChange = (event: DateTimePickerEvent, selectedDate?: Date) => {
  setShowPicker(false);
  if (event.type === 'set' && selectedDate) {
    setDate(selectedDate);
  }
};

Дворівнева архітектура роботи з датами

  1. Шар стану та валідації (Data Layer): оперує чистими екземплярами класу Date або стандартизованими ISO-8601 рядками. Усі порівняння часових інтервалів (Завантаження...) виконуються за таймстемпами.
  2. Шар представлення (Presentation Layer): формує локалізований людиночитабельний підпис («14–18 жовтня 2026») виключно на етапі рендерингу картки, не перетворюючи цей текст назад у модель форми.

Як уникнути перекриття форми клавіатурою

Поява програмної клавіатури (Soft Keyboard) на мобільному пристрої є динамічною зміною геометрії вікна перегляду, яка фізично перекриває до 50% площі екрана.

+---------------------------+       +---------------------------+
| Screen Header             |       | Screen Header             |
+---------------------------+       +---------------------------+
| Field: Name               |       | Field: Name               |
+---------------------------+       +---------------------------+
| Field: Email              |  ==>  | Field: Email (FOCUSED)    |
+---------------------------+       +---------------------------+
| Field: Password           |       | [ SOFT KEYBOARD ]         |
+---------------------------+       | [ Q W E R T Y U I O P ]   |
| Button: [ SUBMIT ] (LOST) |       | [ A S D F G H J K L ]     |
+---------------------------+       +---------------------------+

Фізика оклюзії та системні стратегії:

  1. iOS: системна клавіатура відображається в абсолютному віконному шарі (UIWindow) поверх застосунку. Без спеціального розрахунку зміщення нижня частина форми стає повністю недосяжною.
  2. Android: операційна система оперує режимами windowSoftInputMode (adjustResize або adjustPan). В умовах сучасної Edge-to-Edge верстки потрібна точна синхронізація з системними інсетами.

Комплексний шаблон вирішення: KAV + ScrollView

Для гарантованого збереження контролю над формою використовується трирівнева ієрархія:

SafeAreaView (Edges: top, left, right)
  └── KeyboardAvoidingView (behavior="padding", flex: 1)
        └── ScrollView (keyboardShouldPersistTaps="handled", flex: 1)
              ├── Інпути та контроли форми
              └── Кнопка Submit (навіть при зсуві залишається доступною для скролу)
KeyboardAvoidingView
компонент
Автоматично коригує власні розміри або внутрішні відступи при появі клавіатури.
  • behavior="padding": оптимальна стратегія для iOS, яка додає внутрішній paddingBottom, рівний точній висоті клавіатури.
  • behavior="height" / behavior="position": застосовується на Android за умови специфічних налаштувань віконних режимів.
keyboardVerticalOffset
number
Коригуючий зсув у пікселях, що компенсує висоту кастомного навігаційного хедера, статус-бара або безпечних зон, які KAV не може розрахувати автоматично.
keyboardShouldPersistTaps
'never' | 'always' | 'handled'
Керує ланцюжком обробників дотику (Responder Chain) при активній клавіатурі:
  • never (дефолт): перший тап по кнопці Submit лише закриває клавіатуру, ігноруючи подію onPress.
  • handled: якщо тап припав на інтерактивний елемент (Pressable), клавіатура закривається, але подія onPress негайно виконується. Обов'язковий параметр для всіх мобільних форм!
keyboardDismissMode
'none' | 'on-drag' | 'interactive'
Дозволяє закривати клавіатуру природним жестом прокручування списку (on-drag).
Keyboard.dismiss()
імперативний метод
Програмне закриття клавіатури після успішного відправлення форми або при переході між екранами.


Архітектура валідації: Client-Side UX проти Server-Side Security

Валідація даних у розподілених мобільних системах функціонує на двох взаємодоповнюючих рівнях:

+-------------------------------------------------------------------------+
|                  ДВОРІВНЕВА АРХІТЕКТУРА ВАЛІДАЦІЇ                       |
+-------------------------------------------------------------------------+
|  РІВЕНЬ 1: КЛІЄНТСЬКИЙ ШАР (UX)      |  РІВЕНЬ 2: СЕРВЕРНИЙ ШАР (SECURITY)|
|  • Миттєвий зворотний зв'язок        |  • Абсолютний захист бізнес-правил |
|  • Працює без доступу до мережі      |  • Захист від скомпрометованих API |
|  • Підсвічування рамок та підказки   |  • Криптографічні перевірки та DB  |
|  • Інструменти: Zod + React Hook Form|  • Інструменти: Backend DTO / Zod  |
+--------------------------------------+----------------------------------+

Принципи ергономічного відображення помилок:

  1. Локалізація та контекст: текст помилки рендериться безпосередньо під відповідним інпутом (errors.field?.message), а не в модальному Alert.alert().
  2. Візуальне кодування: рамка помилкового поля забарвлюється у семантичний колір colors.danger.
  3. Неблокуючий ввід: користувач повинен мати можливість вільно друкувати; помилки первинно відображаються після спроби Submit (mode: 'onSubmit'), а під час виправлення зникають у реальному часі (reValidateMode: 'onChange').


React Hook Form + Zod: надійний стек валідації

Коли мобільний додаток містить більше ніж 2–3 поля вводу, класичний підхід на базі локального стану useState перетворюється на джерело багів та проблем із продуктивністю:

  • Проблема ре-рендерів: кожне натискання клавіші оновлює useState, змушуючи React перемальовувати весь екран разом із важкими списками, анімаціями та сусідніми компонентами.
  • Величезний boilerplate: для форми на 7 полів доводиться створювати 7 окремих useState для значень, 7 станів для помилок, окремий isSubmitting, ручні функції скидання та десятки перевірок if (text.length < 2).
  • Фрагментована валідація: правила перевірки розкидані по обробниках подій або змішані з кодом UI.

Сучасним стандартом в екосистемі React та React Native є тандем двох спеціалізованих інструментів: React Hook Form (керування станом форми та життєвим циклом) та Zod (декларативний опис та валідація схем даних).

┌─────────────────────────────────────────────────────────────────────────────┐
│                           ВЗАЄМОДІЯ RHF ТА ZOD                              │
├──────────────────────────────────────┬──────────────────────────────────────┤
│ 📋 REACT HOOK FORM                   │ 🛡️ ZOD                               │
├──────────────────────────────────────┼──────────────────────────────────────┤
│ • Керує станом полів форми           │ • Описує правила валідації (схему)   │
│ • Не перемальовує зайві компоненти   │ • Автоматично генерує типи TypeScript│
│ • Відстежує submit, dirty, touched   │ • Парсить та трансформує дані (.trim)│
│ • Надає Controller для нативних полів│ • Повертає зрозумілі тексти помилок  │
└──────────────────────────────────────┴──────────────────────────────────────┘

Встановлення необхідних пакетів

Для роботи зв'язки потрібні три пакети:

  1. react-hook-form — ядро керування формою.
  2. zod — декларативний валідатор схем.
  3. @hookform/resolvers — офіційний адаптер, що з'єднує схеми Zod із валідаційним конвеєром React Hook Form.
npx expo install react-hook-form @hookform/resolvers zod

Що таке Zod і як працює схема даних?

Zod — це бібліотека TypeScript-first валідації, яка дозволяє описати структуру очікуваних даних декларативно у вигляді об'єкта-схеми.

Замість написання ручних умов:

// ❌ Ручний підхід: спагетті з перевірок
if (!title || title.trim().length < 2) {
  setError('Назва надто коротка');
}

Ви описуєте контракт даних один раз:

// ✅ Декларативний контракт із Zod
import { z } from 'zod';

export const tripSchema = z.object({
  title: z
    .string({ required_error: "Назва є обов'язковою" })
    .trim()
    .min(2, 'Назва має містити щонайменше 2 символи')
    .max(50, 'Назва не може перевищувати 50 символів'),
  plannedDays: z
    .number()
    .int()
    .min(1, 'Мінімум 1 день')
    .max(30, 'Максимум 30 днів'),
  isPrivate: z.boolean(),
  agreeTerms: z.literal(true, {
    errorMap: () => ({ message: 'Необхідно погодитися з правилами' }),
  }),
});

// Автоматичне виведення типу TypeScript із Zod-схеми:
export type TripFormValues = z.infer<typeof tripSchema>;

!TIP Завдяки z.infer<typeof tripSchema> вам не потрібно писати interface TripFormValues вручну! Тип TypeScript завжди на 100% синхронізований зі схемою валідації. Змінили схему — типи оновилися автоматично.


Чому register не працює в React Native і що таке Controller?

Якщо ви раніше працювали з React Hook Form у браузері, ви пам'ятаєте синтаксис:

// 🌐 Тільки у Web: реєстрація через прямий DOM ref
<input {...register('email')} />

У браузері HTML-елемент <input> сам зберігає введений текст у DOM-дереві (Uncontrolled Input). Бібліотека RHF просто чіпляє на нього нативний ref і зчитує input.value лише під час відправки.

Чому це НЕ працює в React Native?

  1. У React Native немає веб-DOM.
  2. Компоненти TextInput, Switch, Slider, Picker — це нативні віджети iOS (UITextField, UISwitch) та Android (EditText, SwitchCompat).
  3. Нативні контроли вимагають явного двостороннього зв'язку через пропси: передачу значення (value, selectedValue) та підписку на зміну (onChangeText, onValueChange).

Controller як універсальний міст (Adapter Pattern)

Для зв'язку нативних компонентів із React Hook Form використовується компонент <Controller />.

Він реалізує патерн Render Prop: Controller бере на себе всю підписку на внутрішній стан форми і передає у вашу функцію render спеціальний об'єкт field:

<Controller
  control={control}
  name="title"
  render={({ field: { onChange, onBlur, value, ref }, fieldState: { error } }) => (
    <View>
      <TextInput
        ref={ref}
        value={value}
        onChangeText={onChange}
        onBlur={onBlur}
        placeholder="Введіть назву"
      />
      {error && <Text style={styles.errorText}>{error.message}</Text>}
    </View>
  )}
/>

Що містить об'єкт field?

  • field.value — поточне значення поля з внутрішнього сховища RHF (передаємо у value={value} або selectedValue={value}).
  • field.onChange — функція оновлення значення у RHF (передаємо у onChangeText={onChange} або onValueChange={onChange}).
  • field.onBlur — колбек втрати фокусу, який повідомляє RHF, що користувач взаємодіяв із полем (touched = true).
  • field.ref — посилання на віджет для автоматичного фокусування на першому помилковому полі при невдалому сабміті.

Що містить об'єкт fieldState?

  • fieldState.error — об'єкт помилки Zod для цього конкретного поля (error?.message).
  • fieldState.isTouched — чи отримувало поле фокус і чи втрачало його.
  • fieldState.isDirty — чи відрізняється поточне значення від defaultValues.

Універсальне підключення будь-яких контролів через Controller

Завдяки гнучкості Controller, через нього можна підключити абсолютно будь-який компонент введення:

{/* 1. Текстове поле (TextInput) */}
<Controller
  control={control}
  name="name"
  render={({ field: { onChange, onBlur, value } }) => (
    <TextInput value={value} onChangeText={onChange} onBlur={onBlur} />
  )}
/>

{/* 2. Перемикач (Switch) */}
<Controller
  control={control}
  name="isPrivate"
  render={({ field: { onChange, value } }) => (
    <Switch value={value} onValueChange={onChange} />
  )}
/>

{/* 3. Чекбокс (Checkbox) */}
<Controller
  control={control}
  name="agreeTerms"
  render={({ field: { onChange, value } }) => (
    <Checkbox value={value} onValueChange={onChange} />
  )}
/>

{/* 4. Повзунок (Slider) */}
<Controller
  control={control}
  name="plannedDays"
  render={({ field: { onChange, value } }) => (
    <Slider value={value} onValueChange={onChange} minimumValue={1} maximumValue={14} step={1} />
  )}
/>

{/* 5. Селектор (Picker) */}
<Controller
  control={control}
  name="ticketType"
  render={({ field: { onChange, value } }) => (
    <Picker selectedValue={value} onValueChange={onChange}>
      <Picker.Item label="Стандарт" value="standard" />
      <Picker.Item label="VIP" value="vip" />
    </Picker>
  )}
/>

Покроковий алгоритм побудови форми від А до Я

       USER ACTION: Тап по кнопці «Зберегти»
                         │
                         ▼
             handleSubmit(onValid, onInvalid)
                         │
                         ▼
        zodResolver(schema): schema.safeParse(data)
                         │
        ┌────────────────┴────────────────┐
        ▼                                 ▼
   [ VALID ]                         [ INVALID ]
        │                                 │
        ▼                                 ▼
  onValid(data)                  formState.errors
Чисті типізовані дані           Оновлення UI з текстом
готові до відправки на API       помилок під кожним полем

Крок 1. Опишіть Zod-схему та виведіть тип

import { z } from 'zod';

export const createTripSchema = z.object({
  title: z.string().trim().min(2, 'Мінімум 2 символи'),
  isPrivate: z.boolean(),
});

export type CreateTripFormValues = z.infer<typeof createTripSchema>;

Крок 2. Ініціалізуйте хук useForm

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';

const {
  control,
  handleSubmit,
  reset,
  formState: { errors, isSubmitting, isValid, isDirty },
} = useForm<CreateTripFormValues>({
  resolver: zodResolver(createTripSchema),
  defaultValues: {
    title: '',
    isPrivate: false,
  },
  mode: 'onSubmit',
  reValidateMode: 'onChange',
});

Крок 3. Напишіть функцію обробки валідних даних

const onValid = async (data: CreateTripFormValues) => {
  // data гарантовано відповідає схемі Zod та типу CreateTripFormValues!
  console.log('Відправка на сервер:', data);
  await api.createTrip(data);
  reset(); // Очищення форми до defaultValues після успіху
};

Крок 4. Зв'яжіть кнопку з handleSubmit

<Pressable 
  onPress={handleSubmit(onValid)} 
  disabled={isSubmitting}
  style={styles.submitBtn}
>
  {isSubmitting ? <ActivityIndicator color="#fff" /> : <Text>Зберегти</Text>}
</Pressable>

Повний огляд параметрів конфігурації useForm

resolver
zodResolver(schema)
Адаптер валідації. Автоматично запускає schema.safeParse() під час сабміту та транслює згенеровані помилки Zod у структуру formState.errors.
defaultValues
DefaultValues<T>
Обов'язковий об'єкт початкових значень для всіх зареєстрованих полів форми. Запобігає попередженням React про неконтрольований перехід зі стану undefined до конкретного типу.
mode
'onSubmit' | 'onChange' | 'onBlur' | 'onTouched' | 'all'
Визначає момент первинного запуску валідації. Рекомендований мобільний стандарт — mode: 'onSubmit', який не відволікає користувача червоними помилками до першої спроби відправки.
reValidateMode
'onChange' | 'onBlur' | 'onSubmit'
Визначає алгоритм повторної перевірки після того, як форма була один раз надіслана з помилками. Значення reValidateMode: 'onChange' гарантує, що помилка зникне миттєво, щойно користувач введе коректні дані.
control
Control<T>
Спеціальний службовий об'єкт-контейнер, що передається у всі компоненти <Controller control={control} /> для реєстрації полів у системі підписок.
formState
FormState<T>
Об'єкт стану форми:
  • errors — словник активних помилок { [fieldName]: { message: string } }.
  • isSubmitting — булевий прапорець виконання асинхронного колбека onValid.
  • isDirtytrue, якщо хоча б одне поле було змінене відносно defaultValues.
  • isValid — чи проходить форма валідацію в поточний момент.
watch
(name?: string) => any
Метод реактивного спостереження за значенням одного або декількох полів без запуску валідації (наприклад, для умовного показу блоків форми).
reset
(values?: DefaultValues<T>) => void
Скидає всі поля форми до початкових значень defaultValues та очищає історію помилок errors.

Демо UX помилок

TSXFormErrorsDemo.tsx
iPhone
9:41

Loading…

react-native-web · not a real device


Класифікація антипатернів розробки мобільних форм

АнтипатернСистемна причинаНаслідки для користувача (UX Failure)Інженерне вирішення
Універсальний TextInputЛінь інтегрувати Community-пакети.Одруки в датах («31.02»), невалідні регіони, конфлікти регістрів.Використовувати Picker, Switch, DateTimePicker.
Модальні Alert для помилокІгнорування вбудованого рендерингу errors.Блокування інтерфейсу, відсутність візуальної прив'язки до помилкового поля.Рендерити inline-текст помилки безпосередньо під рамкою поля.
Імперативна валідація в onPressВідсутність декларативної схеми Zod.Каскад спагетті-коду if/else, неможливість повторного використання на бекенді.Описувати єдину схему z.object({ ... }) через zodResolver.
Форма без ScrollViewТестування лише на великих планшетах/емуляторах.Обрізання форми на компактних екранах та при відкритті клавіатури.Обов'язкова обгортка форми у ScrollView із flex: 1.
Втрата тапу (keyboardShouldPersistTaps)Залишено дефолтне значення never.Перший тап по кнопці Submit поглинається закриттям клавіатури; відчуття «зламаної кнопки».Завжди передавати keyboardShouldPersistTaps="handled".
Зайва автокапіталізаціяЗабутий autoCapitalize="none".Спотворення логінів та email (User@domain.com), відмова в авторизації.Явно вимикати autoCapitalize="none" та autoCorrect={false} для технічних полів.
Клієнт як єдиний захистХибне припущення про абсолютну безпеку клієнта.Вразливість до підміни API-пакетів та ін'єкцій невалідних даних.Дублювати Zod-схеми на бекенд-сервері для повної валідації вхідних DTO.

Міні-проєкт: «Реєстрація на подію»

Мета

Окремий застосунок поза Nomad. Нижче — шлях від А до Я: команди, структура, повний код файлів. Можна копіювати й запускати без «домалюйте самі».

Сюжет: реєстрація на умовну подію. Два стани без Router:

  1. Форма (усі типи контролів + RHF + Zod).
  2. Успіх (підсумок + «Ще раз»).

Що має вийти

ПолеКонтрол
Ім’яTextInput
EmailTextInput + keyboardType="email-address"
Місць 1–5Slider
Тип квиткаPicker
НагадуванняSwitch
Згода з правиламиCheckbox + z.literal(true)
КоментарTextInput multiline
SubmitPressable + handleSubmit

Обов’язково: SafeAreaProviderKeyboardAvoidingViewScrollView + keyboardShouldPersistTaps="handled".

Структура

event-signup/
  App.tsx          ← увесь застосунок (повний код нижче)
  package.json
  app.json
  index.ts
  …

Крок 1. Створити проєкт і поставити пакети

1. Скарфолд

npx create-expo-app@latest event-signup -t blank-typescript
cd event-signup

2. Залежності

npx expo install react-native-safe-area-context @react-native-community/slider @react-native-community/checkbox @react-native-picker/picker
npm install react-hook-form @hookform/resolvers zod

Якщо npm свариться на peers:

npm install react-hook-form @hookform/resolvers zod --legacy-peer-deps

3. Запуск після підстановки App.tsx

npx expo start

Відкрийте в Expo Go.

Крок 2. Повний код App.tsx

Повністю замініть файл App.tsx на код нижче (розгорніть collapsible і скопіюйте цілком).

Крок 3. Що всередині (короткий розбір)

Схема

signupSchema — одне місце правил. acceptedRules: z.literal(true) вимагає галочку. ticketType збігається з Picker.

useForm

resolver: zodResolver(signupSchema), defaultValues (згода спочатку false), mode: 'onSubmit', reValidateMode: 'onChange'.

Controller

Кожне поле підключене явно: TextInput → onChangeText; Switch/Checkbox/Slider → onValueChange; Picker → selectedValue / onValueChange.

Два екрани

done === null — форма; інакше — успіх. «Ще раз» скидає done у null.

Крок 4. Ручна перевірка

  1. npx expo start → Expo Go.
  2. Одразу «Зареєструватись» → помилки під полями.
  3. Коректні дані + галочка → екран успіху.
  4. «Ще раз» → порожня форма.
  5. Фокус у коментарі на малому екрані — кнопка доступна скролом.

Критерій «готово»

  • Проєкт створено командами кроку 1
  • App.tsxповний код з collapsible (усі контроли)
  • Порожній submit показує помилки під полями
  • Без згоди немає екрана успіху
  • Успіх + «Ще раз»
  • KAV + ScrollView + keyboardShouldPersistTaps
Zod 3 vs 4. У прикладі синтаксис ближчий до Zod 4 (error у literal). На Zod 3 підставте API повідомлень з docs вашої major-версії — ідея схеми та сама.

Nomad: форма створення поїздки

Навіщо користувачу

До цієї статті кнопка «Нова поїздка» або нічого не робила, або лише логувала в консоль. Тепер вона відкриває екран форми. Людина заповнює назву, регіон зі списку, дати, опис, тривалість повзунком, за бажанням позначає «приватна» / «є план», підтверджує локальне збереження — і після успіху повертається на стрічку, де нова картка лежить зверху.

На картці видно не лише title: мітки «приватна», «є план», орієнтовну кількість днів — щоб було видно, що boolean/slider реально потрапили в модель Trip.

Нитка проєкту

Уже є з попередніх статей (і це не зникає):

  • ThemeProvider, чіпи light/dark/system;
  • Screen, AppText, shared Button;
  • домашня стрічка на FlashList + горизонтальні місця;
  • pull-to-refresh, sticky CTA «Нова поїздка»;
  • mock-поїздки й mock-місця.

Що з’являється саме в цій статті:

ШматокРоль
Пакети RHF, Zod, picker, slider, checkbox, datetimepickerзалежності форми
createTripSchemaправила Zod
TripsProvider / useTripsспільний список між home і формою
TextField, FormRow, DateFieldUI-обгортки контролів
CreateTripFormуся форма + Controller
app/create-trip.tsxекран-маршрут
розширення типу TripisPrivate, plannedDays, hasItinerary

Стан поїздок піднято в Context (TripsProvider), бо форма на іншому екрані, ніж список. Без спільного стейту router.back() не знав би, куди ділась щойно створена поїздка. Пізніше в курсі Context замінять / доповнять RTK — ідея «одне джерело списку» лишиться.

Навігація навмисно тонка.router.push('/create-trip') і router.back() — мінімум Expo Router, щоб форма жила на окремому екрані. Як влаштовані layouts, tabs, typed routes — наступні статті модуля навігації. Зараз достатньо: файл app/create-trip.tsx ↔ адреса /create-trip.

Встановлення

cd /path/to/nomad
npx expo install @react-native-community/datetimepicker @react-native-picker/picker @react-native-community/slider @react-native-community/checkbox
npm install react-hook-form @hookform/resolvers zod

Повний знімок проєкту

Перевірка

  1. Чіпи теми на home працюють.
  2. Списки поїздок і місць — як раніше.
  3. «Нова поїздка» → форма з TextInput, Picker, датами, Slider, Switch, Checkbox.
  4. Порожній submit / без згоди → помилки під полями.
  5. Успіх → картка зверху; мітки приватності / плану.
  6. Pull-to-refresh скидає до mock (локальні зникають — до offline-модуля).

Коміт

cd /path/to/nomad
git add -A
git commit -m "$(cat <<'EOF'
feat: create trip form with validation

Material: content/15.react-native/10.forms-input-validation.md
EOF
)"
git push

У публічному репо коміт з’явиться після push; локально — git pull.

Результат (прев’ю UX)

Host прев’ю без community-пакетів і RHF — імітація екранів. Повний стек — у репо й Expo Go.

TSXNomadCreateTrip.tsx
iPhone
9:41

Loading…

react-native-web · not a real device


Практичні завдання

Базовий рівень

  1. Своїми словами пояснити, коли Switch, а коли Checkbox.
  2. Зібрати TextInput + Switch + Pressable без RHF.
  3. Пояснити, навіщо value + колбек зміни на будь-якому контролі.

Середній рівень

  1. Міні-проєкт «Реєстрація на подію» з RHF + Zod і щонайменше чотирма типами контролів.
  2. У Nomad додати Picker.Item «Закордон» у TRIP_REGIONS і прогнати схему.
  3. Ланцюжок focus між двома TextInput через returnKeyType="next".

Професійний рівень

  1. Порівняти mode: 'onBlur' і onSubmit на формі з 6+ полями — короткий висновок.
  2. Unit-тести на createTripSchema.safeParse (без UI).
  3. Нотатка: Checkbox і New Architecture — що каже warning Expo і який план Б (Pressable + іконка).

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


Що далі

Тепер у вас є словник контролів і спосіб зв’язати їх правилами. Далі — основи Expo Router: тека app/, layout, Tabs і Stack, Link і useRouter. Екран create-trip уже натякає на стопку екранів; наступна стаття робить навігацію свідомою моделлю всього застосунку (вкладки «Поїздки / Місця / Ще»).

TextInput · Pressable · Button · Switch · Slider · Picker · Checkbox · DateTimePicker (Expo) · React Hook Form · Zod · Nomad

Copyright © 2026