Middle: питання на співбесіді з теми «Рендеринг»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
5 питань
Компонент рендериться повторно, коли:
- змінився його стан (
useState,useReducer); - змінився контекст, який він читає;
- перерендерився батько - за замовчуванням разом з ним рендеряться всі дочірні компоненти, незалежно від того, чи змінилися їхні props.
Третій пункт часто дивує. Але рендер - це лише виклик функції й порівняння віртуального DOM; реальний DOM змінюється тільки там, де щось справді змінилося. Зазвичай це дешево.
React.memo пропускає рендер компонента, якщо його props поверхнево не змінилися (Object.is для кожного prop):
const Chart = memo(function Chart({ data, onSelect }) {
/* важкий рендер */
});
Чому memo часто «не працює»: батько передає новий об'єкт чи функцію на кожен рендер:
<Chart data={rows.filter(isVisible)} onSelect={(id) => select(id)} />
// і data, і onSelect - нові посилання щоразу, memo марний
Тут потрібні useMemo для data і useCallback для onSelect - або передавати примітиви.
Коли memo справді доречний: компонент рендериться помітно довго і часто перерендерюється з тими самими props - великі списки, графіки, редактори. Для дрібних компонентів порівняння props може коштувати стільки ж, скільки рендер.
Альтернатива без мемоізації - правильна структура: опустити стан нижче, до компонента, якому він потрібен, або передати важку частину як children - тоді вона не перерендериться разом із батьком.
React Compiler (стабільний з 2025 року) автоматично мемоізує компоненти й значення, і там, де його ввімкнено, ручні memo/useMemo/useCallback здебільшого не потрібні.
useMemo(fn, deps)- кешує результат обчислення між рендерами, доки залежності не змінились.useCallback(fn, deps)- кешує саму функцію (те саме посилання). По суті,useMemo(() => fn, deps).
const visible = useMemo(
() => todos.filter((t) => matches(t, filter)),
[todos, filter],
);
const handleSelect = useCallback((id) => setSelected(id), []);
Коли вони потрібні:
- Справді дороге обчислення - фільтрація чи сортування тисяч елементів, побудова дерева. Перевірити легко:
console.timeнавколо обчислення; якщо це мілісекунди й більше - є сенс. - Стабільне посилання для дочірнього компонента з
memo- інакше нові об'єкт чи функція на кожен рендер скасовують мемоізацію. - Значення в залежностях ефекту - функція чи об'єкт, створені заново на кожен рендер, перезапускали б
useEffectщоразу.
Коли зайві:
- Прості обчислення (
a + b,items.length, форматування рядка) - мемоізація коштує більше за них. - Колбеки для звичайних DOM-елементів (
<button onClick={...}>) - кнопці байдуже, нова це функція чи ні. - «Про всяк випадок» скрізь - код важче читати, а неправильні залежності створюють баги зі застарілими значеннями.
Важливо: useMemo - оптимізація, а не гарантія. React може скинути кеш, тож код має працювати правильно й без нього.
React Compiler робить цю мемоізацію автоматично на етапі збирання. У проєктах, де він увімкнений, ручні useMemo і useCallback здебільшого прибирають.
Стан зберігається не «в компоненті», а в позиції компонента в дереві. React зберігає стан, доки на тому самому місці дерева рендериться той самий тип компонента.
{isFancy ? <Counter fancy /> : <Counter />}
Перемикання isFancy не скидає лічильник: на тому самому місці той самий тип Counter, змінилися лише props.
Стан скидається, коли:
- на тому самому місці з'являється інший тип компонента (
<Counter />→<Timer />) або інший тег (<div>→<section>- скидається все піддерево); - компонент прибрано з дерева і повернуто;
- змінився
key.
key для скидання стану - найважливіший практичний прийом:
<ContactForm key={contact.id} contact={contact} />
Перемкнули контакт - змінився key - React створює новий екземпляр форми з чистим станом. Без key форма зберегла б введений текст від попереднього контакту.
Це заміна поширеному антипатерну «скинути стан в ефекті при зміні props»:
// погано: зайвий рендер зі старими даними, потім скидання
useEffect(() => { setDraft(''); }, [contact.id]);
Пастка - компонент, оголошений усередині компонента:
function Page() {
const [text, setText] = useState('');
function Field() { // нова функція на кожному рендері Page
return <input value={text} onChange={(e) => setText(e.target.value)} />;
}
return <Field />;
}
На кожному рендері Field - новий тип компонента, тож React розмонтовує старий і монтує новий: поле губить фокус після кожної літери. Компоненти оголошують на верхньому рівні модуля.
Зворотний бік - стан «прилипає», коли це не очікується: однакова позиція в різних гілках умови, однаковий key у різних списках. Тоді допомагає різний key для гілок.
Зберегти стан прихованого компонента (вкладки, бічна панель) - не прибирати його з дерева: CSS-приховування, <Activity mode="hidden"> або винести стан угору до батька.
Портал рендерить дочірні елементи в інший вузол DOM, ніж той, де знаходиться компонент:
import { createPortal } from 'react-dom';
function Modal({ open, onClose, children }) {
if (!open) return null;
return createPortal(
<div className="fixed inset-0 grid place-items-center bg-black/50" onClick={onClose}>
<div role="dialog" aria-modal="true" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>,
document.body,
);
}
Навіщо: модальні вікна, підказки, випадні меню мають бути поверх усього. Якщо їхня розмітка глибоко в дереві, батьківські стилі заважають: overflow: hidden обрізає, transform ламає position: fixed, z-index обмежений контекстом накладання батька.
Ключова особливість - події. Портал змінює місце в DOM, але не в дереві React. Події спливають за деревом React:
<div onClick={() => console.log('клік у батьку')}>
<Modal open>
<button>Кнопка у вікні</button> {/* клік спливе до div батька! */}
</Modal>
</div>
Клік по кнопці, яка в DOM лежить у body, все одно викличе onClick батька. Те саме з контекстом: портал бачить провайдери свого предка в React, а не за місцем у DOM.
Це зручно (стан, контекст і обробники працюють як для звичайних дітей), але інколи несподівано - наприклад, «клік поза меню» через contains() у DOM вважатиме клік у порталі зовнішнім.
Що ще потрібно для якісного модального вікна (портал цього не дає):
- фокус усередині вікна і повернення фокусу після закриття;
- закриття по Escape;
- блокування прокрутки сторінки;
aria-modal,aria-labelledby.
Нативна альтернатива - <dialog> з showModal(): браузер сам виносить вікно на верхній шар (без порталу й боротьби із z-index), дає фокус і Escape. Для багатьох випадків простіше за портал.
SSR: на сервері порталів у document.body немає - модальні вікна зазвичай рендерять лише на клієнті (після монтування).
Без обробки помилка під час рендеру будь-якого компонента розмонтовує весь застосунок - користувач бачить порожню сторінку. Межа помилок перехоплює помилки свого піддерева й показує запасний інтерфейс.
Межі помилок досі пишуться лише класами - хука-аналога немає:
class ErrorBoundary extends React.Component {
state = { error: null };
static getDerivedStateFromError(error) {
return { error }; // наступний рендер покаже fallback
}
componentDidCatch(error, info) {
reportError(error, info.componentStack); // у моніторинг
}
render() {
if (this.state.error) return this.props.fallback;
return this.props.children;
}
}
<ErrorBoundary fallback={<p>Віджет не завантажився</p>}>
<RevenueChart />
</ErrorBoundary>
На практиці частіше беруть готовий пакет react-error-boundary - з функцією скидання (resetErrorBoundary), ключами для автоматичного скидання і хуком useErrorBoundary.
Що межа помилок ловить: помилки під час рендеру, у хуках життєвого циклу, в конструкторах дочірніх компонентів - усе, що відбувається, коли React викликає ваш код у дереві. Також відхилений проміс, прочитаний через use().
Чого НЕ ловить:
- обробники подій (
onClick) - там звичайнийtry/catch; - асинхронний код -
setTimeout, проміси безuse(), колбеки запитів; - помилки в самій межі - вони йдуть до наступної межі вище;
- серверний рендер - на сервері помилки обробляються інакше.
Щоб передати помилку з обробника чи асинхронного коду в межу, її «кидають» під час рендеру: зберегти в стан і throw у рендері, або showBoundary(error) з react-error-boundary.
Де ставити межі:
- верхня - на весь застосунок, щоб замість білого екрана був зрозумілий текст і кнопка оновлення;
- навколо незалежних віджетів - графік, коментарі, рекомендації: зламаний віджет не зносить сторінку;
- на рівні маршруту - роутери (React Router, Next.js
error.tsx) мають власні межі.
React 19: опції кореня onCaughtError і onUncaughtError у createRoot - централізоване логування помилок, перехоплених межами і неперехоплених.