useEffect и useLayoutEffect — это два хука в React для работы с побочными эффектами, но они выполняются в разное время жизненного цикла компонента:
| Характеристика | useEffect | useLayoutEffect |
|---|---|---|
| Время выполнения | Асинхронно, после отрисовки | Синхронно, до отрисовки |
| Блокирует рендеринг | Нет | Да |
| Видимость пользователю | Может вызвать мерцание | Нет мерцания |
| Производительность | Лучше | Хуже |
Когда использовать:
Хуки useEffect и useLayoutEffect в React решают схожие задачи, но имеют ключевые различия во времени выполнения и влиянии на производительность.
Оба хука позволяют выполнять побочные эффекты в функциональных компонентах, но различаются по времени выполнения:
import { useEffect, useLayoutEffect, useState } from 'react';
function Component() {
const [count, setCount] = useState(0);
// Выполняется асинхронно после отрисовки
useEffect(() => {
console.log('useEffect: Компонент отрисован');
});
// Выполняется синхронно до отрисовки
useLayoutEffect(() => {
console.log('useLayoutEffect: Перед отрисовкой');
});
return <div>Счетчик: {count}</div>;
}useEffect выполняется асинхронно после отрисовки:
function WithUseEffect() {
const [count, setCount] = useState(0);
useEffect(() => {
// Выполняется после отрисовки
console.log('useEffect выполнен');
document.title = `Счетчик: ${count}`;
}, [count]);
return <div>Счетчик: {count}</div>;
}useLayoutEffect выполняется синхронно до отрисовки:
function WithUseLayoutEffect() {
const [count, setCount] = useState(0);
useLayoutEffect(() => {
// Выполняется до отрисовки
console.log('useLayoutEffect выполнен');
document.title = `Счетчик: ${count}`;
}, [count]);
return <div>Счетчик: {count}</div>;
}useEffect не блокирует рендеринг:
function NonBlockingEffect() {
const [width, setWidth] = useState(0);
useEffect(() => {
// Не блокирует рендеринг
const calculateWidth = () => {
// Тяжелые вычисления
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += i;
}
setWidth(result % 1000);
};
calculateWidth();
}, []);
return <div>Ширина: {width}px</div>;
}useLayoutEffect блокирует рендеринг:
function BlockingEffect() {
const [width, setWidth] = useState(0);
useLayoutEffect(() => {
// Блокирует рендеринг до завершения
const calculateWidth = () => {
// Тяжелые вычисления
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += i;
}
setWidth(result % 1000);
};
calculateWidth();
}, []);
return <div>Ширина: {width}px</div>;
}useEffect может вызвать мерцание:
function FlickeringComponent() {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
// Может вызвать мерцание
if (isVisible) {
document.body.style.backgroundColor = 'yellow';
} else {
document.body.style.backgroundColor = 'white';
}
}, [isVisible]);
return (
<div>
<p>Фон может мерцать</p>
<button onClick={() => setIsVisible(!isVisible)}>
Переключить фон
</button>
</div>
);
}useLayoutEffect не вызывает мерцания:
function NonFlickeringComponent() {
const [isVisible, setIsVisible] = useState(false);
useLayoutEffect(() => {
// Не вызывает мерцания
if (isVisible) {
document.body.style.backgroundColor = 'yellow';
} else {
document.body.style.backgroundColor = 'white';
}
// Очистка
return () => {
document.body.style.backgroundColor = 'white';
};
}, [isVisible]);
return (
<div>
<p>Фон не мерцает</p>
<button onClick={() => setIsVisible(!isVisible)}>
Переключить фон
</button>
</div>
);
}// Загрузка данных с API
useEffect(() => {
async function fetchData() {
const response = await fetch('/api/data');
const data = await response.json();
setData(data);
}
fetchData();
}, []);
// Подписка на события
useEffect(() => {
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
}
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);// Измерение размеров элемента
useLayoutEffect(() => {
const element = ref.current;
if (element) {
setDimensions({
width: element.offsetWidth,
height: element.offsetHeight
});
}
}, []);
// Синхронизация скролла
useLayoutEffect(() => {
const savedPosition = localStorage.getItem('scrollPosition');
if (savedPosition) {
window.scrollTo(0, parseInt(savedPosition));
}
}, []);import { useLayoutEffect, useRef, useState } from 'react';
function ElementMeasurer() {
const ref = useRef();
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useLayoutEffect(() => {
if (ref.current) {
const { offsetWidth, offsetHeight } = ref.current;
setDimensions({
width: offsetWidth,
height: offsetHeight
});
}
}, []);
return (
<div>
<div ref={ref} style={{
width: '200px',
height: '100px',
backgroundColor: 'lightblue'
}}>
Измеряемый элемент
</div>
<p>Ширина: {dimensions.width}px</p>
<p>Высота: {dimensions.height}px</p>
</div>
);
}import { useLayoutEffect, useState } from 'react';
function FlickerPrevention() {
const [count, setCount] = useState(0);
const [color, setColor] = useState('red');
useLayoutEffect(() => {
// Меняем цвет до отрисовки, избегая мерцания
setColor(count % 2 === 0 ? 'red' : 'blue');
}, [count]);
return (
<div style={{ color }}>
<p>Цветной текст: {count}</p>
<button onClick={() => setCount(count + 1)}>
Увеличить
</button>
</div>
);
}import { useLayoutEffect, useRef } from 'react';
function DOMSynchronization() {
const inputRef = useRef();
useLayoutEffect(() => {
// Фокусируем элемент до отрисовки
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
return (
<div>
<input
ref={inputRef}
placeholder="Этот input получит фокус без мерцания"
/>
</div>
);
}useEffect и useLayoutEffect — мощные инструменты для работы с побочными эффектами в React:
✅ Когда использовать useEffect:
✅ Когда использовать useLayoutEffect:
Ключевые моменты:
Понимание разницы между этими хуками позволяет оптимизировать производительность и улучшить пользовательский опыт в React-приложениях.
Хотите больше статей для подготовки к собеседованиям? Подписывайтесь на EasyAdvice(@AleksandrEmolov_EasyAdvice), добавляйте сайт в закладки и совершенствуйтесь каждый день 💪