В чем разница между useEffect и useLayoutEffect?

👨‍💻 Frontend Developer 🟡 Часто попадается 🎚️ Средний
#React #Hooks

Краткий ответ

useEffect и useLayoutEffect — это два хука в React для работы с побочными эффектами, но они выполняются в разное время жизненного цикла компонента:

ХарактеристикаuseEffectuseLayoutEffect
Время выполненияАсинхронно, после отрисовкиСинхронно, до отрисовки
Блокирует рендерингНетДа
Видимость пользователюМожет вызвать мерцаниеНет мерцания
ПроизводительностьЛучшеХуже

Когда использовать:

  • useEffect — для большинства побочных эффектов (API, подписки, таймеры)
  • useLayoutEffect — когда нужно читать или изменять DOM до отрисовки (измерения, анимации, синхронизация)

Полный ответ

Хуки useEffect и useLayoutEffect в React решают схожие задачи, но имеют ключевые различия во времени выполнения и влиянии на производительность.

Что такое useEffect и useLayoutEffect

Оба хука позволяют выполнять побочные эффекты в функциональных компонентах, но различаются по времени выполнения:

  • useEffect — выполняется асинхронно после отрисовки компонента в DOM
  • useLayoutEffect — выполняется синхронно до отрисовки изменений в браузере
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>;
}

Основные различия

1. Время выполнения

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>;
}

2. Блокировка рендеринга

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>;
}

3. Видимость пользователю

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>
  );
}

Когда использовать каждый хук

Использовать useEffect для:

  1. Асинхронных операций — загрузка данных, API вызовы
  2. Подписок — события, WebSockets
  3. Таймеров — setTimeout, setInterval
  4. Логирования — отслеживание изменений
// Загрузка данных с 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 для:

  1. Чтения DOM — измерения элементов
  2. Синхронизации — точное позиционирование
  3. Анимаций — предотвращение мерцаний
  4. Мутаций DOM — изменение до отрисовки
// Измерение размеров элемента
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));
  }
}, []);

Практические примеры

1. Измерение элементов

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>
  );
}

2. Предотвращение мерцания

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>
  );
}

3. Синхронизация с DOM

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:

  • Асинхронные операции (API, подписки)
  • Когда мерцание не важно
  • Для большинства побочных эффектов

Когда использовать useLayoutEffect:

  • Чтение/изменение DOM до отрисовки
  • Когда нужно избежать мерцания
  • Для точной синхронизации с DOM

Ключевые моменты:

  • useEffect выполняется асинхронно после отрисовки
  • useLayoutEffect выполняется синхронно до отрисовки
  • useLayoutEffect блокирует рендеринг
  • Для большинства случаев подходит useEffect
  • useLayoutEffect используется в специфических сценариях

Понимание разницы между этими хуками позволяет оптимизировать производительность и улучшить пользовательский опыт в React-приложениях.


Хотите больше статей для подготовки к собеседованиям? Подписывайтесь на EasyAdvice(@AleksandrEmolov_EasyAdvice), добавляйте сайт в закладки и совершенствуйтесь каждый день 💪