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" відправить форму.
Поле не реагує на введення. Причина - 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 попередить про перемикання режиму. Початкове значення - ''.
Доступна форма - та, якою можна користуватися з клавіатури й зчитувачем екрана. Основа - правильні зв'язки між полями, мітками й повідомленнями.
Мітка для кожного поля. 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.