Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як працює useLayoutEffect у React і чим він відрізняється від useEffect?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**useLayoutEffect** виконується синхронно після того, як React зафіксував зміни в DOM, але до відображення в браузері. **useEffect** - після paint. ```jsx useLayoutEffect(() => { readDOM(); updateDOM(); }, []); // синхронно, до paint useEffect(() => { fetchData(); }, []); // асинхронно, після paint ``` **Головне правило:** треба прочитати або змінити DOM до того, як користувач це побачить - `useLayoutEffect`. Все інше - `useEffect`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**useLayoutEffect** - хук React, який запускається синхронно після того, як React зафіксував зміни в DOM, але до того, як браузер намалював екран. **useEffect** запускається асинхронно після рендерингу. Ця різниця у часі визначає все. ## Теорія ### TL;DR - `useLayoutEffect` - це як перевірити лист перед тим, як запечатати конверт. `useEffect` - це перевірити, що ти надіслав, вже після доставки. - Головна відмінність: `useLayoutEffect` блокує відображення браузера до завершення. `useEffect` - ні. - Потрібно прочитати або змінити DOM до того, як користувач це побачить? `useLayoutEffect`. Запити до API, підписки, аналітика? `useEffect`. - `useLayoutEffect` не виконується на сервері (SSR). `useEffect` працює нормально. - У React 18 `useLayoutEffect` досі запускається з найвищим пріоритетом і може блокувати concurrent-переходи. ### Швидкий приклад ```jsx import { useEffect, useLayoutEffect, useRef } from "react"; function Demo() { const ref = useRef(null); useLayoutEffect(() => { // Запускається ДО відображення - користувач не бачить цей стан if (ref.current) ref.current.style.background = "red"; }); useEffect(() => { // Запускається ПІСЛЯ відображення - перезаписує layout effect if (ref.current) ref.current.style.background = "blue"; }); return <div ref={ref} style={{ width: 100, height: 100, background: "green" }} />; } // Що бачить користувач: синій блок. // Порядок: зелений (JSX) -> червоний (layout effect, до paint) -> синій (effect, після paint). // Миготіння відсутнє - користувач бачить тільки синій. ``` Обидва ефекти спрацьовують на кожному рендері (без масиву залежностей). Layout effect встановлює червоний, але ця зміна не потрапляє на екран, бо відображення ще заблоковане. Звичайний ефект спрацьовує після paint і перезаписує колір на синій. ### Ключова відмінність `useLayoutEffect` підключається до коміт-фази React у `commitLayoutEffects` і виконується синхронно, блокуючи `requestAnimationFrame` до завершення. Тобто ти можеш викликати `getBoundingClientRect()` і отримати актуальні значення поточного DOM. `useEffect` планується як пасивний ефект з нижчим пріоритетом, вже після того, як браузер відобразив зміни. Будь-яке звернення до DOM всередині `useEffect` може отримати вже застарілі дані - звідси і характерне миготіння тултипів або дропдаунів при першому рендері. ### Коли використовувати useLayoutEffect, а коли useEffect - **Виміряти розміри елементів до того, як користувач їх побачить**: `useLayoutEffect`. Запобігає стрибку елемента з одної позиції в іншу. - **Прочитати позицію скролу або виділення тексту після коміту**: `useLayoutEffect`. Дані відповідають поточному стану DOM, а не застарілому знімку. - **Позиціонування тултипів, дропдаунів, поповерів**: `useLayoutEffect`. Читання і запис layout в одному синхронному вікні усуває миготіння. - **Запити до API, підписки, аналітика, таймери**: `useEffect`. Їм не потрібен доступ до DOM до відображення. Блокувати paint заради них - зайві витрати. ### Таблиця порівняння | Аспект | useLayoutEffect | useEffect | |---|---|---| | **Час запуску** | Синхронно, після коміту, до paint | Асинхронно, після paint | | **Блокує відображення?** | Так | Ні | | **Підходить для** | Вимірювання DOM, корекція стилів | API-запити, підписки, логування | | **Поведінка при SSR** | Пропускається (немає DOM) | Виконується нормально | | **Вартість для продуктивності** | Вища (блокує UI-потік) | Нижча (неблокуючий) | | **Strict Mode (dev)** | Подвійний виклик | Подвійний виклик | | **React 18 concurrent mode** | Найвищий пріоритет, може блокувати переходи | Пасивний, відкладений | ### Як React планує ці ефекти всередині Файбер-рекончайлер React запускає `useLayoutEffect` у `commitLayoutEffects` з пріоритетом `ImmediateSchedulerPriority`. Це синхронний цикл по fiber-деревах, який завершується ще до того, як `requestAnimationFrame` отримує свій слот. `useEffect` потрапляє в `commitPassiveMountEffects` через `scheduleCallback(NormalSchedulerPriority)` - пасивна черга, браузер спочатку малює, і тільки потім React обробляє пасивні ефекти. Але є нюанс. У React 18 concurrent mode `useLayoutEffect` досі запускається з максимальним пріоритетом. Важка логіка всередині нього може затримати переходи з `useTransition`, бо він не поступається місцем відкладеним оновленням. На практиці я бачив кодові бази, де `useLayoutEffect` використовували для запитів до API «на всяк випадок». Мережевий запит асинхронний в будь-якому разі, тому це нічого не прискорює. Але блокує paint на кожному рендері. Відкрий вкладку Performance в Chrome DevTools, запиши один рендер - побачиш це blocking time прямо в таймлайні. ### Типові помилки **Запити до API всередині useLayoutEffect** ```jsx // Неправильно - блокує paint без жодної причини useLayoutEffect(() => { fetch('/api/data').then(setData); }, []); // Правильно useEffect(() => { fetch('/api/data').then(setData); }, []); ``` Fetch все одно асинхронний. `useLayoutEffect` не зробить його швидшим, але затримає перший paint. **Ігнорування серверного рендерингу** ```jsx // Виводить попередження при SSR - немає DOM на сервері useLayoutEffect(() => { document.title = 'My App'; }, []); // Безпечні варіанти: useEffect(() => { document.title = 'My App'; }, []); // Або з перевіркою: useLayoutEffect(() => { if (typeof window === 'undefined') return; document.title = 'My App'; }, []); ``` Next.js і Remix пропускають `useLayoutEffect` на сервері, але виводять попередження в dev-режимі. Якщо бачиш таке попередження - або переходь на `useEffect`, або додай перевірку `typeof window`. **Нестабільні залежності викликають нескінченний цикл** ```jsx // Нескінченний цикл - новий об'єкт на кожному рендері useLayoutEffect(() => { applyStyle(elementRef.current, style); }, [style]); // style = { color: 'red' } створюється заново щоразу // Виправлення через useMemo const style = useMemo(() => ({ color: 'red' }), []); useLayoutEffect(() => { applyStyle(elementRef.current, style); }, [style]); ``` Це не унікальна проблема `useLayoutEffect`, але синхронне батчінг-ре-рендерів робить цикл щільнішим і складнішим для відладки. **Анімації, які оновлюються на кожному кадрі** ```jsx // Блокує requestAnimationFrame при кожній зміні value useLayoutEffect(() => { gsap.to(element, { x: 100 }); }, [value]); // Краще - нехай бібліотека анімацій керує своїм циклом useEffect(() => { gsap.to(element, { x: 100 }); }, [value]); ``` ### Де використовується в реальних проектах - **React Window / React Virtual**: вимірює висоту рядків до віртуалізації скролу через `useLayoutEffect`, щоб перший рендер показував правильні зміщення. - **Framer Motion**: читає DOM-bounds до відображення для layout-анімацій (пропc `layout` всередині). - **Floating UI / React Tooltip**: позиціонування тултипів і поповерів завжди через `useLayoutEffect` - елементи з'являються в правильних координатах одразу. - **TanStack Table**: вимірювання ширини колонок при resize через `useLayoutEffect`, щоб уникнути миготіння під час перетягування. - **React Spring**: layout effects для синхронного читання стилів в фізичних анімаціях. ### Питання для співбесіди **Q:** Який точний порядок виконання `useLayoutEffect` і `useEffect` з урахуванням cleanup при повторному рендері? **A:** При монтуванні: спочатку layout effect, потім passive effect. При ре-рендері: cleanup layout effect, layout effect, cleanup passive effect, passive effect. Layout завжди перед passive. Це важливо, якщо один ефект залежить від стану, який встановлює інший. **Q:** Що відбувається з `useLayoutEffect` при SSR у Next.js? **A:** React пропускає його і виводить попередження в dev-режимі. Компонент рендериться, але ефект на сервері не виконується. Для серверно-безпечних side effects використовуй `useEffect`. **Q:** Скільки разів спрацьовує `useLayoutEffect` у React 18 Strict Mode? **A:** Двічі в режимі розробки. React монтує, запускає ефект, запускає cleanup, потім монтує знову. Це виявляє відсутню логіку очищення. У продакшені - один раз. **Q:** Що таке `useInsertionEffect` і чим він відрізняється? **A:** `useInsertionEffect` (React 18+) запускається ще до мутацій DOM - він розроблений для CSS-in-JS бібліотек, які вставляють стилі. Читати layout у ньому не можна. `useLayoutEffect` запускається після мутацій і може читати layout. Для роботи з DOM `useLayoutEffect` залишається правильним вибором. **Q (senior):** Чи може `useLayoutEffect` заблокувати concurrent-переходи в React 18? **A:** Так. Він виконується з пріоритетом `ImmediateSchedulerPriority`, тобто до того, як React поступиться місцем відкладеним оновленням від `useTransition`. Важка синхронна логіка всередині затримає завершення переходу і помітно сповільнить UI. ## Приклади ### Позиціонування тултипу без миготіння Канонічний кейс. Без `useLayoutEffect` тултип на мить з'явиться в позиції `{top: 0, left: 0}` і тільки потім стрибне туди, де треба. ```jsx import { useLayoutEffect, useState, useRef } from "react"; function Tooltip({ children, text }) { const [position, setPosition] = useState({ top: 0, left: 0 }); const triggerRef = useRef(null); const tooltipRef = useRef(null); useLayoutEffect(() => { const trigger = triggerRef.current.getBoundingClientRect(); const tooltip = tooltipRef.current.getBoundingClientRect(); // Розраховуємо позицію до відображення - стрибка не буде setPosition({ top: trigger.bottom + 8, left: Math.max(8, trigger.left + (trigger.width - tooltip.width) / 2), }); }, []); return ( <> <span ref={triggerRef}>{children}</span> <div ref={tooltipRef} style={{ position: "fixed", top: position.top, left: position.left, background: "#111", color: "#fff", padding: "4px 8px", borderRadius: 4, }} > {text} </div> </> ); } ``` Обидва rect читаються синхронно, позиція розраховується, і React ре-рендерить з правильними координатами до відображення. Бібліотека Floating UI використовує цей самий підхід всередині. ### Визначення переповнення тексту до першого paint ```jsx import { useState, useLayoutEffect, useRef } from "react"; function TruncatedLabel({ text }) { const [isOverflowing, setIsOverflowing] = useState(false); const ref = useRef(null); useLayoutEffect(() => { if (ref.current) { // scrollWidth > clientWidth означає, що текст обрізається setIsOverflowing(ref.current.scrollWidth > ref.current.clientWidth); } }, [text]); return ( <span ref={ref} title={isOverflowing ? text : undefined} style={{ display: "block", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", maxWidth: 200, }} > {text} </span> ); } ``` Якби тут був `useEffect`, компонент спочатку відобразився б без атрибута title, і тільки потім додав його. З `useLayoutEffect` вимірювання і ре-рендер відбуваються до першого paint - користувач бачить одразу правильний стан. ### useEffect для звичайного кейсу Не кожна задача потребує `useLayoutEffect`. Якщо немає вимірювань або мутацій DOM до відображення - `useEffect` є правильним вибором. ```jsx import { useState, useEffect } from "react"; function UserProfile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { let cancelled = false; fetch(`/api/users/${userId}`) .then((r) => r.json()) .then((data) => { if (!cancelled) setUser(data); }); return () => { cancelled = true; }; }, [userId]); if (!user) return <p>Завантаження...</p>; return <p>{user.name}</p>; } ``` Запиту до API не потрібен доступ до DOM до відображення. `useLayoutEffect` тут заблокував би перший paint на час мережевого запиту - додаткова затримка без жодної користі.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.