Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Junior: питання на співбесіді з теми «Форми й Actions»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

4 питання

Неконтрольоване поле зберігає значення саме, у DOM. React лише задає початкове значення:

<input name="title" defaultValue="Чернетка" />

Значення читають у момент відправки - з FormData чи через ref.

Контрольоване поле - значення живе в стані React, а поле лише показує його:

const [title, setTitle] = useState('');

<input value={title} onChange={(e) => setTitle(e.target.value)} />

Кожне натискання клавіші - onChange → новий стан → новий рендер з новим value.

Коли контрольоване:

  • значення потрібне під час введення: лічильник символів, пошук «на льоту», валідація в реальному часі;
  • поля залежать одне від одного (вибір країни змінює список міст);
  • значення треба форматувати чи обмежувати (лише цифри, верхній регістр);
  • кнопка «Зберегти» активна лише при зміненій формі.

Коли неконтрольоване:

  • значення потрібне лише при відправці - звичайна форма з <form action> чи onSubmit і FormData;
  • великі форми, де рендер на кожну літеру помітно гальмує;
  • <input type="file"> - завжди неконтрольоване: значення файлового поля задати з коду не можна.

Правила, які не можна порушувати:

  • поле не може бути одночасно контрольованим і неконтрольованим, і не повинне перемикатися між режимами. Типова причина - value={user.name}, де name спершу undefined: поле стартує неконтрольованим, а потім стає контрольованим. Початкове значення - порожній рядок '', а не null чи undefined;
  • для чекбоксів - checked/defaultChecked, а не value.

Бібліотеки форм (React Hook Form) за замовчуванням працюють з неконтрольованими полями саме заради продуктивності, а стан помилок і «брудності» ведуть окремо.

Докладніше в документації: input: керування полем через стан

Класичний спосіб - onSubmit:

function SearchForm() {
  function handleSubmit(e) {
    e.preventDefault();                       // інакше браузер перезавантажить сторінку
    const data = new FormData(e.currentTarget);
    search(data.get('q'));
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="q" />
      <button type="submit">Шукати</button>
    </form>
  );
}

React 19 - функція в пропі action:

function SearchForm() {
  async function searchAction(formData) {
    await search(formData.get('q'));
  }

  return (
    <form action={searchAction}>
      <input name="q" />
      <button type="submit">Шукати</button>
    </form>
  );
}

Чим action відрізняється:

  • preventDefault() не потрібен - React сам перехоплює відправку;
  • функція отримує FormData одразу аргументом;
  • виконується всередині Transition - на ній працюють useFormStatus (стан відправки для кнопки), useActionState (результат дії) і useOptimistic;
  • після успішного виконання неконтрольовані поля форми скидаються автоматично. Якщо поле має лишитися заповненим (пошук), значення треба зберігати в стані чи повертати з дії;
  • метод HTTP завжди POST, незалежно від атрибута method;
  • різні кнопки можуть викликати різні дії через formAction:
<button type="submit">Опублікувати</button>
<button formAction={saveDraft}>Зберегти чернетку</button>

Коли все ж onSubmit: складна клієнтська валідація перед відправкою, бібліотеки форм зі своїм handleSubmit (React Hook Form), потреба повністю контролювати процес.

Пастка для обох способів: кнопка без type усередині форми - це type="submit". Кнопка «Скасувати» чи «Додати рядок» без type="button" відправить форму.

Докладніше в документації: form: обробка відправки

Поле не реагує на введення. Причина - value без onChange:

<input value={title} />   // поле лише для читання

Контрольоване поле завжди показує те, що передано у value. Користувач натискає клавішу, браузер змінює поле, React повертає старе значення. У консолі - попередження: «You provided a value prop to a form field without an onChange handler».

Варіанти виправлення:

  • потрібне лише початкове значення - defaultValue={title};
  • поле має керуватися станом - додати onChange={(e) => setTitle(e.target.value)};
  • поле справді лише для читання - явно readOnly.

Курсор стрибає в кінець чи на початок. Типові причини:

1. Асинхронне оновлення стану:

onChange={async (e) => {
  await validate(e.target.value);
  setTitle(e.target.value);   // запізно - поле вже перемальовано зі старим значенням
}}

Стан контрольованого поля треба оновлювати синхронно в обробнику, а асинхронну перевірку робити окремо.

2. Поле перестворюється на кожен рендер:

  • компонент поля оголошено всередині іншого компонента - на кожен рендер це новий тип компонента, React знищує старий <input> і створює новий;
  • змінюється key поля (наприклад, key={Math.random()}).
function Form() {
  // помилка: новий компонент на кожен рендер
  const Field = (props) => <input {...props} />;
  return <Field value={title} onChange={...} />;
}

Компоненти оголошують на верхньому рівні модуля.

3. Перетворення значення на ходу (setTitle(e.target.value.toUpperCase())) іноді збиває позицію курсора в деяких браузерах. Для форматування (телефон, сума) краще форматувати при виході з поля чи спеціальні бібліотеки масок.

value={null} чи undefined робить поле неконтрольованим - а коли значення з'явиться, React попередить про перемикання режиму. Початкове значення - ''.

Докладніше в документації: input: розв'язання проблем

Доступна форма - та, якою можна користуватися з клавіатури й зчитувачем екрана. Основа - правильні зв'язки між полями, мітками й повідомленнями.

Мітка для кожного поля. placeholder мітку не замінює: він зникає при введенні й погано читається.

<label htmlFor="email">Електронна адреса</label>
<input id="email" name="email" type="email" />

У JSX - htmlFor, а не for.

Унікальні id - через useId. Жорсткий id="email" ламається, якщо компонент поля використано на сторінці двічі. useId генерує стабільний унікальний id, однаковий на сервері й клієнті (без розбіжностей гідратації):

function TextField({ label, error, ...props }) {
  const id = useId();
  const errorId = `${id}-error`;

  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input
        id={id}
        aria-invalid={error ? true : undefined}
        aria-describedby={error ? errorId : undefined}
        {...props}
      />
      {error && <p id={errorId} role="alert">{error}</p>}
    </div>
  );
}
  • aria-invalid - зчитувач оголосить, що поле з помилкою;
  • aria-describedby - текст помилки прочитається разом з полем;
  • role="alert" - повідомлення оголошується одразу при появі.

useId - не для ключів у списках: ключі мають походити з даних.

Інші правила:

  • групи радіокнопок і чекбоксів - у <fieldset> з <legend>;
  • фокус на першій помилці після невдалої відправки (React Hook Form робить це сам - shouldFocusError);
  • не вимикати кнопку відправки «доки форма невалідна» - користувач не розуміє, що не так. Краще дозволити відправку й показати помилки;
  • правильні типи полів (type="email", inputMode="numeric", autoComplete="email") - правильна клавіатура на телефоні й автозаповнення;
  • стан відправки оголошувати текстом («Зберігаємо...»), а не лише спінером.

Перевірка: пройти форму лише клавіатурою (Tab, Enter, пробіл) і з увімкненим VoiceOver/NVDA.

Докладніше в документації: useId