Skip to main content

Яка різниця між useEffect i useLayoutEffect?

useEffect і useLayoutEffect - різниця лише в таймінгу: один запускається після того, як браузер намалював екран, інший блокує малювання до завершення.

Теорія

TL;DR

  • useEffect спрацьовує після того, як браузер відмалював екран (асинхронно, після paint)
  • useLayoutEffect спрацьовує до paint, блокуючи браузер до завершення колбека (синхронно, до paint)
  • Аналогія: useEffect - це прибиральники, що приходять вже після того, як гості побачили безлад. useLayoutEffect прибирає до того, як гості зайшли
  • Потрібні DOM-вимірювання або виправлення без мерехтіння? useLayoutEffect. Все інше? useEffect
  • useLayoutEffect видає попередження в SSR (Node.js); useEffect ні

Швидкий приклад

jsx
function Box() { const [count, setCount] = useState(0); const ref = useRef(); // Запускається ПІСЛЯ paint - юзер спочатку бачить стару ширину, потім стрибок useEffect(() => { ref.current.style.width = `${count * 20}px`; }, [count]); // Заміни на це - ширина оновиться до paint, без мерехтіння // useLayoutEffect(() => { // ref.current.style.width = `${count * 20}px`; // }, [count]); return ( <div ref={ref} style={{ background: '#eee' }} onClick={() => setCount(c => c + 1)}> Клік: {count} </div> ); }

З useEffect браузер спочатку малює стару ширину, потім ефект спрацьовує і елемент стрибає. Заміна на useLayoutEffect виправляє ширину ще до того, як браузер малює перший піксель.

Головна різниця

Обидва хуки мають однаковий синтаксис, cleanup-функція працює так само. Відрізняється тільки момент запуску всередині фази commit у React. useLayoutEffect запускається синхронно після мутацій DOM, але ще до paint браузера. useEffect ставиться в чергу після paint з нижчим пріоритетом. Різниця в мілісекундах мала, але для ока помітна, коли потрібні DOM-вимірювання або виправлення стилів. Я бачив таку проблему в продакшені: тултіп, позиція якого вираховувалась у useEffect, помітно зіскакував на місце при кожному рендері.

Коли що використовувати

  • Вимірювання DOM (ширина, висота, scroll position) перед рендером → useLayoutEffect
  • Виправлення макету щоб уникнути видимого стрибка або мерехтіння → useLayoutEffect
  • Анімації, що читають layout перед записом значень → useLayoutEffect
  • Запити до API, таймери, підписки на події → useEffect
  • Все, що не стосується видимого DOM-макету → useEffect

Таблиця порівняння

АспектuseEffectuseLayoutEffect
ТаймінгПісля paint (асинхронно)До paint (синхронно)
Блокує paint браузера?НіТак
Читання DOM-layoutВикликає мерехтінняБезпечно, відповідає реальному стану
Ризик для продуктивностіМінімальнийМоже затримати рендер якщо колбек важкий
SSR (Node.js)БезпечноВидає попередження
Типове застосуванняAPI, підписки, таймериDOM-вимірювання, виправлення стилів

Як це працює всередині

Під час фази commit у React колбеки useLayoutEffect запускаються синхронно відразу після мутацій DOM, до paint браузера. Колбеки useEffect ставляться в чергу після paint через внутрішній React Scheduler з нижчим пріоритетом. У React 18 з concurrent mode useLayoutEffect все одно залишається синхронним і блокує transitions. Для несрочної роботи всередині startTransition використовуй useEffect.

Типові помилки

Виправлення scroll у useEffect:

jsx
// Неправильно: юзер спочатку бачить верх сторінки, потім стрибок useEffect(() => { ref.current.scrollTop = 100; }, []); // Правильно: прокрутка відбувається до першого paint useLayoutEffect(() => { ref.current.scrollTop = 100; }, []);

Важкі обчислення в useLayoutEffect:

jsx
// Неправильно: блокує paint на 100мс+, інтерфейс зависає useLayoutEffect(() => { for (let i = 0; i < 1_000_000; i++) { /* важка робота */ } }, []);

Колбек useLayoutEffect має виконуватись менше 5мс. Все важче переноси в useEffect.

Забуття про SSR: useLayoutEffect видає попередження в Node.js: Warning: useLayoutEffect does nothing on the server. Якщо компонент рендериться на сервері (Next.js, Remix), переходь на useEffect або перевіряй typeof window !== 'undefined'.

Де зустрічається в реальних проектах

  • Framer Motion читає DOM-layout у useLayoutEffect перед записом значень анімації
  • Next.js <Image> використовує useLayoutEffect для розміру placeholder
  • TanStack Query v5 використовує useEffect для запитів, useLayoutEffect для синхронізації кешу таблиць
  • React DevTools використовує useLayoutEffect для вимірювань в інспекторі
  • Redux Toolkit використовує useEffect для підписок на store

Питання на співбесіді

Q: Чи може useLayoutEffect погіршити продуктивність?
A: Так, бо блокує paint. Якщо колбек займає більше кількох мілісекунд, браузерний фрейм затримується і інтерфейс підвисає. Chrome DevTools позначає це як long task.

Q: Що відбувається з useLayoutEffect при SSR?
A: React виводить попередження і хук нічого не робить на сервері. useEffect безпечний для SSR, бо виконується тільки в браузері.

Q: Який порядок спрацювання cleanup?
A: Обидва підтримують cleanup-функції. Cleanup у useLayoutEffect запускається синхронно до наступного layout-ефекту. У useEffect - асинхронно перед наступним ефектом.

Q: У React 18 з concurrent mode, useLayoutEffect все ще блокує?
A: Так, залишається синхронним і до paint навіть у concurrent mode. startTransition відкладає оновлення стану, але useLayoutEffect все одно спрацьовує до paint. Для несрочних побічних ефектів у transitions використовуй useEffect.

Приклади

Вимірювання висоти елемента перед paint

jsx
function TodoItem({ text, onDelete }) { const ref = useRef(); const [height, setHeight] = useState(0); useLayoutEffect(() => { // Вимірюємо до paint - без видимого стрибка макету setHeight(ref.current.getBoundingClientRect().height); }); return ( <div ref={ref} style={{ minHeight: `${height}px`, transition: 'height 0.2s' }}> {text} <button onClick={onDelete}>Видалити</button> </div> ); }

getBoundingClientRect читає реальні розміри DOM. В useEffect браузер спочатку намалював би неправильну висоту - видно стрибок при додаванні або видаленні елемента. useLayoutEffect вимірює і виправляє до того, як юзер щось побачить.

Завантаження даних з useEffect

jsx
function UserProfile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { let cancelled = false; fetch(`/api/users/${userId}`) .then(res => res.json()) .then(data => { if (!cancelled) setUser(data); }); return () => { cancelled = true; }; }, [userId]); if (!user) return <p>Завантаження...</p>; return <p>{user.name}</p>; }

Завантаження даних не має відношення до pre-paint layout. useEffect тут правильний вибір. Прапорець cancelled запобігає оновленню стану розмонтованого компонента - поширене джерело попереджень про витік пам'яті в React DevTools.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?