Питання на співбесіді з React
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
100 питань
setCount(count + 1) не змінює змінну count - він планує новий рендер, у якому useState поверне нове значення. У поточному виконанні функції count лишається тим, яким був на початку рендеру. Стан - це «знімок» для конкретного рендеру.
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
setCount(count + 1);
console.log(count); // 0 - ще старий знімок
}
// після кліку count стане 1, а не 2
}
Обидва виклики порахували 0 + 1.
Функціональне оновлення - передати функцію, яка отримає актуальне значення з черги оновлень:
setCount((c) => c + 1);
setCount((c) => c + 1); // тепер 2
Коли потрібне: коли нове значення залежить від попереднього і оновлень може бути кілька поспіль, а також в асинхронному коді й таймерах, де count у замиканні може бути застарілим.
Ще правила стану:
- Не змінювати стан на місці:
items.push(x); setItems(items)- React порівнює посилання, бачить той самий масив і може не перерендерити. Правильно:setItems([...items, x]). - React групує оновлення (batching): кілька
setStateв одному обробнику дають один рендер.
Два правила:
- Хуки викликають лише на верхньому рівні компонента чи власного хука: не в умовах, циклах, вкладених функціях, після раннього
return. - Хуки викликають лише з функціональних компонентів і власних хуків, не зі звичайних функцій чи класів.
Чому: React не знає імен ваших станів. Він зберігає стан компонента як список і зіставляє виклики хуків з елементами списку за порядком: перший useState - перший елемент, другий - другий. Якщо на одному рендері виклик пропустили через умову, усі наступні хуки «з'їдуть» на чужі значення.
// Неправильно
if (isLoggedIn) {
const [name, setName] = useState('');
}
const [theme, setTheme] = useState('light'); // візьме стан name, якщо умова змінилася
// Правильно: умова всередині, хук - завжди
const [name, setName] = useState('');
const displayName = isLoggedIn ? name : 'Гість';
Як не помилитися: плагін eslint-plugin-react-hooks перевіряє обидва правила й залежності useEffect. Його варто тримати увімкненим завжди.
Виняток: хук use (React 19) можна викликати в умовах і циклах - він читає Promise чи контекст і не зберігає стан у списку.
React вирішує, чи перерендерити компонент, порівнюючи посилання: новий стан має бути новим об'єктом. Зміна старого об'єкта «на місці» не помітна для React - і ламає логіку порівняння в memo, useEffect, useMemo.
Об'єкти - копія з розгортанням:
const [user, setUser] = useState({ name: 'Оля', address: { city: 'Київ', street: '' } });
// неправильно: мутація
user.address.city = 'Львів';
setUser(user);
// правильно: нові об'єкти на кожному рівні шляху до зміни
setUser({
...user,
address: { ...user.address, city: 'Львів' },
});
Розгортання (...) поверхневе - копіювати треба кожен рівень, що змінюється.
Масиви - методи, що повертають новий масив:
| Дія | Замість мутації | Без мутації |
|---|---|---|
| додати | push, unshift |
[...items, item] |
| видалити | splice |
items.filter((i) => i.id !== id) |
| змінити | items[i] = x |
items.map((i) => (i.id === id ? { ...i, done: true } : i)) |
| вставити | splice |
[...items.slice(0, n), item, ...items.slice(n)] |
| сортувати | sort, reverse |
items.toSorted(...), items.toReversed() |
Пастка зі «скопійованим» масивом: [...items] копіює масив, але елементи-об'єкти лишаються ті самі. copy[0].done = true змінює об'єкт і в старому стані.
Глибоко вкладений стан робить такі оновлення громіздкими. Два виходи:
- Immer (
use-immer): пишете «мутацію», а бібліотека створює новий незмінний об'єкт:
updateUser((draft) => {
draft.address.city = 'Львів';
});
- сплощити стан - зберігати сутності за
idв окремих полях замість глибокої вкладеності.
Функціональне оновлення для залежності від попереднього стану: setItems((prev) => [...prev, item]).
Чому це важливо не лише для рендеру: незмінність дає «знімки» історії (скасування дій), дешеве порівняння для оптимізацій і передбачувані ефекти.
useEffect синхронізує компонент із зовнішньою системою: підписка, таймер, WebSocket, сторонній віджет, запит до API.
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect(); // очищення
}, [roomId]);
Масив залежностей визначає, коли ефект перезапускається:
| Варіант | Коли виконується |
|---|---|
| без масиву | після кожного рендеру |
[] |
один раз після монтування (і очищення при розмонтуванні) |
[roomId] |
після монтування і щоразу, коли roomId змінився |
Значення порівнюються через Object.is: для об'єктів і функцій, створених під час рендеру, кожен рендер - «нове» значення, і ефект перезапускається щоразу.
Функція очищення виконується:
- перед кожним повторним запуском ефекту - зі старими значеннями (відключитися від старої кімнати перед підключенням до нової);
- при розмонтуванні компонента.
Без очищення - витоки: таймери продовжують працювати, підписки накопичуються, відповіді на старі запити перезаписують нові дані.
Залежності не обирають - їх перелічують. Кожне реактивне значення (props, стан, змінні й функції з тіла компонента), що використовується в ефекті, має бути в масиві. Правило лінтера react-hooks/exhaustive-deps перевіряє це автоматично. Якщо ефект перезапускається надто часто, рішення - змінити код (винести функцію всередину ефекту, функціональне оновлення стану, useEffectEvent), а не викинути залежність з масиву.
StrictMode в режимі розробки монтує компонент двічі: ефект → очищення → ефект. Це навмисна перевірка, що очищення написане правильно. Якщо від цього щось ламається (подвійний запит із побічним ефектом, подвійна аналітика), - бракує очищення.
Коли ефект не потрібен: обчислення з props чи стану (рахувати під час рендеру), реакція на дію користувача (робити в обробнику події), скидання стану при зміні props (через key).
Для доступності елементи форм зв'язують через id: <label htmlFor>, aria-describedby, aria-labelledby. Але компонент поля може бути на сторінці кілька разів - жорстко прописаний id="email" дублюватиметься.
useId генерує унікальний ідентифікатор для кожного екземпляра компонента:
import { useId } from 'react';
function PasswordField({ label, hint }) {
const id = useId();
return (
<>
<label htmlFor={id}>{label}</label>
<input id={id} type="password" aria-describedby={`${id}-hint`} />
<p id={`${id}-hint`}>{hint}</p>
</>
);
}
Один виклик - базовий id, а пов'язані ідентифікатори утворюють з суфіксами.
Чому не Math.random() чи лічильник:
- серверний рендер: HTML генерується на сервері, потім React «гідратує» його в браузері.
Math.random()дасть на клієнті інший id, ніж на сервері, - помилка розбіжності гідратації, а зв'язкиlabel-inputзламаються; - кожен рендер - новий id, якщо генерувати в тілі компонента, і атрибути постійно змінюватимуться;
- глобальний лічильник залежить від порядку рендеру, який на сервері й клієнті може відрізнятися.
useId будує ідентифікатор зі шляху компонента в дереві - він однаковий на сервері й клієнті і стабільний між рендерами.
Чого useId не робить:
- не для
keyу списках - ключі мають походити з даних (item.id); - не для пошуку елементів через
document.getElementById- для доступу до DOM єuseRef; - не гарантує конкретного формату - не будуйте на ньому CSS-селектори.
Кілька застосунків React на одній сторінці (наприклад, «острівці» на серверній сторінці) можуть згенерувати однакові id. Для них задається префікс: createRoot(el, { identifierPrefix: 'search-' }) і такий самий на сервері.
JSX - синтаксичне розширення JavaScript, що дозволяє писати розмітку прямо в коді. Браузер його не розуміє: компілятор (Babel, esbuild, SWC, TypeScript) перетворює JSX на виклики функцій.
const element = <h1 className="title">Привіт, {user.name}</h1>;
// після компіляції (новий JSX transform)
import { jsx as _jsx } from 'react/jsx-runtime';
const element = _jsx('h1', { className: 'title', children: ['Привіт, ', user.name] });
Результат - звичайний об'єкт-опис («React element»), а не DOM-вузол. React потім вирішує, як привести DOM у відповідність.
Відмінності від HTML:
classNameзамістьclass,htmlForзамістьfor- бо це JavaScript-властивості.- Атрибути в camelCase:
onClick,tabIndex;style- об'єкт:style={{ marginTop: 8 }}. - Кожен тег закривається:
<img />,<br />. - Компонент повертає один корінь. Для кількох елементів - фрагмент
<>...</>. - У фігурних дужках - вирази, не інструкції:
{isAdmin && <Badge />},{items.map(...)}, але неifчиfor.
Безпека: значення в {} автоматично екрануються, тож рядок з <script> виведеться як текст. Небезпечний лише явний dangerouslySetInnerHTML.
Компонент - функція, що повертає JSX. Назва з великої літери обов'язкова: <profile /> React вважатиме HTML-тегом, <Profile /> - компонентом.
Під час оновлення React порівнює новий список елементів зі старим. key каже, який елемент нового списку відповідає якому елементу старого - щоб зберегти його стан і DOM.
<ul>
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
Без key React виводить попередження й зіставляє елементи за позицією.
З індексом як key - те саме зіставлення за позицією, лише без попередження. Поки список лише дописують у кінець, проблем немає. Після видалення, вставки на початок чи сортування:
- стан компонентів (введений текст у полі, чекбокс, розгорнутість) «переїжджає» до сусіднього елемента;
- React оновлює вміст багатьох елементів замість переміщення одного.
Правила для key:
- Стабільний: той самий для того самого елемента між рендерами -
idз бази. - Унікальний серед сусідів (не глобально).
- Не генерувати під час рендеру:
key={Math.random()}чиcrypto.randomUUID()змушують React щоразу перестворювати всі елементи й губити стан. Якщо в даних немає ID, його генерують один раз - при створенні запису.
Корисний прийом: зміна key компонента примусово перестворює його з чистим станом: <Profile key={userId} userId={userId} /> - форма скинеться при переході до іншого користувача.
У JSX немає директив на кшталт v-if - умови пишуть звичайним JavaScript.
Тернарний оператор - один із двох варіантів:
{isLoggedIn ? <UserMenu /> : <LoginButton />}
&& - показати або нічого:
{hasError && <ErrorMessage />}
Ранній return - для цілих станів компонента:
if (isLoading) return <Spinner />;
if (!user) return null; // null - нічого не рендерити
return <Profile user={user} />;
Пастка з && і числами:
{items.length && <List items={items} />}
Якщо масив порожній, items.length дорівнює 0. Оператор && повертає ліву частину, коли вона хибна, - тобто 0. А React рендерить число 0 як текст. Користувач бачить загадковий «0» на сторінці.
React не рендерить лише null, undefined, false, true і порожній рядок. Число 0 і NaN він показує.
Як правильно:
{items.length > 0 && <List items={items} />}
{!!items.length && <List items={items} />}
{items.length ? <List items={items} /> : null}
Ліва частина && має бути логічним значенням, а не просто «чимось, що буває хибним».
Інші поради:
- складні умови - у змінну чи окремий компонент, а не вкладені тернарні оператори в JSX;
- мапа станів замість ланцюжка умов:
const content = {
loading: <Spinner />,
error: <ErrorMessage />,
empty: <EmptyState />,
}[status] ?? <List items={items} />;
- умова змінює дерево: якщо в гілках однакові компоненти на тому самому місці, React може зберегти їхній стан між гілками. Коли стан має скидатися, різні гілки отримують різний
key.
Приховати, а не прибрати - hidden чи CSS-клас: компонент і його стан лишаються. У React 19.2+ для цього є й <Activity mode="hidden">.
Props - аргументи компонента. Батько передає їх як атрибути JSX, дитина отримує одним об'єктом (зазвичай з деструктуризацією):
function Avatar({ user, size = 48 }) {
return <img src={user.avatarUrl} alt={user.name} width={size} height={size} />;
}
<Avatar user={currentUser} size={64} />
Правила props:
- лише для читання - компонент не змінює свої props. Якщо значення має змінюватися, це стан (у батька чи в самому компоненті);
- значення за замовчуванням - у деструктуризації (
size = 48); спрацьовує дляundefined, але не дляnull; - розгортання
<Avatar {...props} />зручне для обгорток, але приховує, що саме передається, - використовувати помірно.
children - вміст між тегами компонента:
function Card({ title, children }) {
return (
<section className="rounded border p-4">
<h2>{title}</h2>
{children}
</section>
);
}
<Card title="Профіль">
<Avatar user={user} />
<p>{user.bio}</p>
</Card>
Композиція замість успадкування. У React не створюють class SpecialCard extends Card. Спеціалізація - через props і вкладення:
- «слоти» - кілька props з JSX:
<Layout sidebar={<Nav />} header={<Header />}>...</Layout>; - спеціалізований компонент рендерить загальний з потрібними props:
function WarningCard(props) { return <Card {...props} tone="warning" />; }; - поведінку перевикористовують через власні хуки, а не базові класи.
Чому це краще: компоненти незалежні, їх легко комбінувати в будь-якому порядку, а зміни в «базовому» компоненті не ламають ієрархію нащадків.
Корисний наслідок children для продуктивності: якщо дорогий компонент передано як children, батько, що змінює свій стан, не перерендерює його - JSX-елемент створено вище й не змінився.
Prop drilling - коли дані передаються через багато рівнів, які їх не використовують. Спершу варто спробувати композицію (передати готовий JSX вниз), а вже потім Context.
Компонент повертає один кореневий елемент JSX. Щоб повернути кілька елементів без зайвої обгортки в DOM, використовують Fragment:
function UserInfo({ user }) {
return (
<>
<dt>Ім'я</dt>
<dd>{user.name}</dd>
</>
);
}
<>...</> - скорочений запис <Fragment>...</Fragment>. У DOM потрапляють лише dt і dd.
Навіщо не обгортати в <div>:
- валідний HTML: усередині
<dl>,<ul>,<table>,<tr>допустимі лише певні елементи.<div>між<tr>і<td>- некоректна розмітка й попередження React; - CSS-розкладка: у flex- чи grid-контейнері зайвий
<div>стає окремим елементом сітки й ламає розташування; - менше DOM - трохи легше для браузера на великих сторінках.
Fragment з key - коли фрагмент рендериться в циклі. Скорочений запис <> атрибутів не приймає, потрібна повна форма:
import { Fragment } from 'react';
function Glossary({ items }) {
return (
<dl>
{items.map((item) => (
<Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.description}</dd>
</Fragment>
))}
</dl>
);
}
Що варто знати:
key- єдиний атрибут, який приймає Fragment (в експериментальних версіях з'являється щеref);- Fragment не має власного DOM-вузла - на нього не можна повісити обробник події чи клас;
- збереження стану: React однаково трактує
<>з дітьми і масив дітей на верхньому рівні, тож перехід між<><Child /></>і<Child />не скидає стан. А от зміна позиції в дереві чиkey- скидає.
Повернути масив (return [<li key="a" />, <li key="b" />]) теж можна, але потрібні key на кожному елементі - Fragment читається простіше.
Коли два компоненти мають показувати чи змінювати одні й ті самі дані, стан не може жити в кожному з них окремо - копії розійдуться. Його переносять у найближчого спільного предка й передають вниз через props.
Було - кожна панель сама вирішує, чи відкрита:
function Panel({ title, children }) {
const [isOpen, setIsOpen] = useState(false);
// ...
}
Вимога «відкрита лише одна панель одночасно» так не реалізується: панелі не знають одна про одну.
Стало - стан у батька, панелі керовані:
function Accordion() {
const [openId, setOpenId] = useState('about');
return (
<>
<Panel title="Про нас" isOpen={openId === 'about'} onOpen={() => setOpenId('about')}>...</Panel>
<Panel title="Доставка" isOpen={openId === 'delivery'} onOpen={() => setOpenId('delivery')}>...</Panel>
</>
);
}
function Panel({ title, isOpen, onOpen, children }) {
return (
<section>
<button onClick={onOpen}>{title}</button>
{isOpen && children}
</section>
);
}
Panel став керованим (controlled): ним керує батько через props. Компонент зі своїм внутрішнім станом - некерований (uncontrolled).
Принцип «одного джерела правди»: для кожного шматка стану є один компонент-власник. Інші отримують значення через props і повідомляють про зміни через колбеки.
Куди піднімати:
- до найближчого спільного предка всіх, хто використовує дані, - не вище. Стан занадто високо змушує перерендерюватися велику частину дерева й ускладнює компоненти;
- якщо спільний предок дуже далеко і дані треба передавати через багато рівнів («prop drilling»), - спершу композиція (передати готовий JSX через
children), потім Context.
Зворотний процес теж корисний: якщо стан використовується лише в одному компоненті, його варто опустити туди (state colocation) - менше зайвих рендерів і простіші батьки.
Типова помилка - дублювати стан: копіювати props у локальний стан дитини (useState(props.value)). Копія не оновиться, коли батько змінить значення. Або компонент керований (значення з props), або некерований (власний стан з початковим значенням), - змішування дає розсинхронізацію.
Надлишковий стан - значення, яке можна обчислити з props чи іншого стану. Його зберігання - джерело помилок, бо дві копії правди рано чи пізно розходяться.
Погано:
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState(''); // надлишкове
function handleFirstNameChange(e) {
setFirstName(e.target.value);
setFullName(e.target.value + ' ' + lastName); // треба не забути оновити тут
}
Добре - обчислювати під час рендеру:
const fullName = `${firstName} ${lastName}`;
Типові випадки надлишкового стану:
- відфільтрований чи відсортований список поруч з оригіналом:
const visibleTodos = todos.filter((t) => (showDone ? true : !t.done));
- кількості й суми:
items.length,items.reduce(...); - обраний елемент як копія об'єкта - краще зберігати
selectedIdі знаходити об'єкт:items.find((i) => i.id === selectedId). Інакше зміна елемента в списку не потрапить у «обраний»; - прапорці, що випливають з даних:
isEmpty,hasErrors,canSubmit.
Антипатерн - синхронізація ефектом:
const [visibleTodos, setVisibleTodos] = useState([]);
useEffect(() => {
setVisibleTodos(todos.filter(...));
}, [todos]);
Зайвий рендер зі старими даними, потім ще один - з новими. Плюс місце для помилки, якщо забути залежність.
А якщо обчислення дороге? Спершу виміряти. Якщо справді повільно - useMemo (або React Compiler зробить це автоматично):
const visibleTodos = useMemo(() => filterTodos(todos, query), [todos, query]);
Коли копія з props доречна: коли це початкове значення, яке далі живе своїм життям, - і тоді props варто назвати відповідно (initialColor, defaultValue), щоб було зрозуміло, що подальші зміни props не враховуються.
Перевірка структури стану: для кожного значення в useState запитати: «чи можна його обчислити з інших?». Якщо так - це не стан.
Найпростіше завантаження в ефекті:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let ignore = false;
fetch(`/api/users/${userId}`)
.then((response) => response.json())
.then((data) => {
if (!ignore) setUser(data);
});
return () => {
ignore = true;
};
}, [userId]);
if (!user) return <Spinner />;
return <h1>{user.name}</h1>;
}
Навіщо змінна ignore. Користувач швидко перемикає профілі: userId 1, потім 2. Летять два запити. Якщо відповідь для 1 прийде пізніше за відповідь для 2, без перевірки вона перезапише стан - на екрані профіль 2-го користувача з даними 1-го. Функція очищення попереднього ефекту ставить ignore = true, і застаріла відповідь ігнорується.
Ще краще - скасувати запит:
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then((r) => r.json())
.then(setUser)
.catch((error) => {
if (error.name !== 'AbortError') setError(error);
});
return () => controller.abort();
}, [userId]);
StrictMode у розробці запускає ефект двічі - з очищенням між запусками. Перший запит скасовується. Це нормально й саме перевіряє, що очищення написане.
Чого бракує цьому підходу (і чому документація React радить не писати таке вручну у великих застосунках):
- немає кешу: повернення на сторінку - новий запит і знову спінер;
- «водоспад» запитів: дочірні компоненти починають завантаження лише після рендеру батька;
- стани помилки й завантаження - у кожному компоненті вручну;
- немає попереднього завантаження і повторного запиту при поверненні на вкладку;
- серверний рендер: ефекти на сервері не виконуються.
Альтернативи:
- фреймворк (Next.js, React Router з завантажувачами) - дані завантажуються до рендеру маршруту;
- TanStack Query чи SWR - кеш, повтори, скасування, синхронізація;
use()з Suspense і промісом зі стабільного джерела.
useEffect для даних лишається прийнятним для невеликих застосунків і простих випадків - але з очищенням.
Докладніше в документації: Синхронізація з ефектами: завантаження даних
Для стану, яким користуються компоненти на різних рівнях дерева (кошик, список завдань, налаштування), React пропонує поєднання useReducer + Context без сторонніх бібліотек.
const TasksContext = createContext(null);
const TasksDispatchContext = createContext(null);
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, []);
return (
<TasksContext value={tasks}>
<TasksDispatchContext value={dispatch}>
{children}
</TasksDispatchContext>
</TasksContext>
);
}
export function useTasks() {
const tasks = useContext(TasksContext);
if (tasks === null) throw new Error('useTasks має бути всередині TasksProvider');
return tasks;
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
У React 19 сам контекст можна рендерити як провайдер (<TasksContext value={...}>); старий запис <TasksContext.Provider> теж працює.
Використання в будь-якому компоненті під провайдером:
function AddTask() {
const dispatch = useTasksDispatch();
return <button onClick={() => dispatch({ type: 'added', text: 'Нове завдання' })}>Додати</button>;
}
function TaskList() {
const tasks = useTasks();
return tasks.map((task) => <Task key={task.id} task={task} />);
}
Чому два контексти, а не один:
dispatchстабільний - не змінюється між рендерами;- компоненти, яким потрібна лише відправка дій (кнопки, форми), підписуються лише на
TasksDispatchContextі не перерендерюються, коли змінюється список завдань.
Переваги підходу:
- логіка змін - в одному редукторі, її легко тестувати;
- компоненти не передають колбеки через десятки рівнів;
- власні хуки (
useTasks) приховують деталі й перевіряють наявність провайдера.
Обмеження, через які переходять на сховища:
- будь-яка зміна значення контексту перерендерює всіх його споживачів - навіть тих, кому потрібна лише частина даних. Немає селекторів «підпишись лише на це поле»;
- провайдери, що вкладаються один в один для кожної частини стану, ускладнюють структуру;
- немає інструментів налагодження «з коробки», проміжних обробників (middleware), збереження стану.
Для частих оновлень і великого спільного стану - Zustand, Redux Toolkit чи подібні з підписками на частину стану.
Докладніше в документації: Масштабування з reducer і context
Неконтрольоване поле зберігає значення саме, у 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: керування полем через стан
Питання з реальних технічних співбесід - 100 питань у 8 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.
- Теми
- Хуки 15 Рендеринг 15 Стан і дані 12 Форми й Actions 12 Next.js, Inertia й маршрутизація 12 TypeScript та інструменти 12 Продуктивність 12 Тестування 10
Готуєтесь до співбесіди не просто так: зараз на сайті 145 відкритих вакансій Laravel і PHP. Переглянути вакансії