Чому useEffect не гарантовано викликається одразу після commit?
1. Що робить commit-фаза
У React кожен цикл оновлення ділиться на дві великі частини:
| Фаза | Що робить | Можна перервати? |
|---|---|---|
| Render Phase | React обчислює, що потрібно змінити (нове Virtual DOM) | Так |
| Commit Phase | React застосовує зміни в реальний DOM | Ні |
Після commit-фази DOM вже оновлений, екран перемальований, і React готовий викликати ефекти (useEffect, useLayoutEffect, componentDidMount тощо).
Але, важливо, не всі ефекти однакові за пріоритетом.
2. Різні типи ефектів
React розрізняє два типи побічних ефектів:
| Тип | Хук | Коли викликається |
|---|---|---|
| Синхронний (layout) | useLayoutEffect | одразу після commit, до відображення на екрані |
| Пасивний (passive) | useEffect | асинхронно, після відображення і браузерного paint-тіку |
3. Чому useEffect() - асинхронний
Після commit React:
- Оновлює DOM.
- Повідомляє браузеру: "готово, можна перемалювати екран".
- Чекає, поки браузер закінчить paint.
- Тільки потім викликає всі "пасивні" ефекти (
useEffect).
Це потрібно, щоб:
- UI став видимим якомога швидше;
- ефекти (мережеві запити, таймери, підписки тощо) не гальмували перемальовування;
- браузер залишався чутливим (scroll, input, animation не блокуються).
Приклад (різниця між useLayoutEffect і useEffect)
useLayoutEffect(() => {
console.log('useLayoutEffect'); // синхронно після оновлення DOM
});
useEffect(() => {
console.log('useEffect'); // асинхронно, після paint
});Лог у консолі:
render
commit
useLayoutEffect ← одразу після commit
PAINT (браузер)
useEffect ← після відображення4. Чому це важливо в React 18 і concurrent rendering
У React 18 рендер став переривним і пріоритетним (Concurrent Rendering). Через це React може:
- відкладати виклик
useEffect(), якщо є більш термінові завдання (наприклад, користувач клікнув); - скасовувати ефекти, якщо рендер скасувався до commit;
- об'єднувати кілька рендерів в один commit, а ефекти викликати тільки один раз після нього.
Тому React гарантує лише одне:
useEffect() буде викликаний після того, як DOM вже оновлений і браузер показав зміни,
але не обов'язково миттєво - це може бути на наступному event loop-тіку.
5. А якщо потрібен ефект "одразу після commit"?
Тоді потрібно використовувати useLayoutEffect():
useLayoutEffect(() => {
console.log('Синхронно після commit');
});useLayoutEffect:
- викликається синхронно одразу після того, як React оновив DOM;
- блокує відображення до завершення колбека;
- ідеально підходить для вимірювань (наприклад,
getBoundingClientRect).
Але використовувати
useLayoutEffectбез потреби небезпечно, він може затримувати відображення UI. Для більшості побічних ефектів (fetch, підписки, таймери) кращеuseEffect.
6. Візуально
Render phase
↓
Commit phase (DOM оновлено)
↓
→ useLayoutEffect (одразу)
↓
Браузер відобразив (paint)
↓
→ useEffect (асинхронно)7. Підсумок
React не викликає
useEffectодразу після commit, тому щоuseEffect- це пасивний ефект, який виконується асинхронно після того, як браузер завершив paint.Це гарантує, що:
- UI відобразиться максимально швидко,
- ефекти не блокують інтерфейс,
- concurrent rendering залишається чутливим.
| Хук | Коли викликається | Використовувати для |
|---|---|---|
useLayoutEffect | одразу після commit (синхронно) | вимірювання DOM, синхронізація layout |
useEffect | після paint (асинхронно) | побічні ефекти, запити, підписки |
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.