← Назад к вопросам

UseEffect выполняется синхронно или асинхронно

2.0 Middle🔥 301 комментариев
#React

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

UseEffect: синхронно или асинхронно

useEffect выполняется асинхронно, но с важными нюансами. Понимание этого критично для правильной работы с побочными эффектами в React.

Как работает useEffect

Этап 1: Рендер компонента (синхронно)

function MyComponent() {
  // Это выполняется синхронно при рендере
  const [count, setCount] = useState(0)
  console.log('Render: count =', count)
  
  return <div>{count}</div>
}

Этап 2: Обновление DOM (синхронно)

// React обновляет DOM
// Это происходит после рендера, но ДО useEffect

Этап 3: Выполнение useEffect (асинхронно)

function MyComponent() {
  const [count, setCount] = useState(0)
  
  // Это выполняется ПОСЛЕ рендера и обновления DOM
  useEffect(() => {
    console.log('useEffect: count =', count)
  }, [count])
  
  return <div>{count}</div>
}

Порядок выполнения

1. Рендер функции компонента
2. Обновление DOM
3. Запуск useEffect (асинхронно)
4. Очистка от старого useEffect
5. Выполнение нового useEffect

Пример временной последовательности

function Counter() {
  const [count, setCount] = useState(0)
  
  console.log('1. Render') // Выполняется ПЕРВЫМ (синхронно)
  
  useEffect(() => {
    console.log('3. useEffect') // Выполняется ТРЕТЬИМ (асинхронно)
    return () => {
      console.log('4. useEffect cleanup') // Перед следующим эффектом
    }
  }, [count])
  
  // Клик на кнопку
  const handleClick = () => {
    console.log('2. Click handler') // Выполняется ВТОРЫМ
    setCount(count + 1)
  }
  
  return <button onClick={handleClick}>Count: {count}</button>
}

// Вывод при клике:
// 1. Render
// 2. Click handler
// 4. useEffect cleanup (от предыдущего эффекта)
// 1. Render (повторный рендер)
// 3. useEffect (новый эффект)

Когда useEffect выполняется

После рендера, но асинхронно:

function Example() {
  // СИНХРОННО при рендере
  const [data, setData] = useState(null)
  console.log('Render, data =', data)
  
  // АСИНХРОННО после рендера
  useEffect(() => {
    console.log('Effect, fetching data...')
    fetch('/api/data').then(response => {
      setData(response.data)
    })
  }, [])
  
  return <div>{data ? 'Loaded' : 'Loading'}</div>
}

// Порядок логов:
// 1. Render, data = null
// 2. Effect, fetching data...
// (когда пришли данные)
// 3. Render, data = { ... }

useLayoutEffect - синхронная альтернатива

Если нужно выполнить что-то синхронно, используй useLayoutEffect:

function Example() {
  const ref = useRef()
  
  // useEffect - выполняется асинхронно (ПОСЛЕ отрисовки)
  useEffect(() => {
    console.log('useEffect: rect =', ref.current?.getBoundingClientRect())
  }, [])
  
  // useLayoutEffect - выполняется синхронно (ДО отрисовки)
  useLayoutEffect(() => {
    console.log('useLayoutEffect: rect =', ref.current?.getBoundingClientRect())
  }, [])
  
  return <div ref={ref}>Element</div>
}

// Порядок:
// 1. Рендер
// 2. useLayoutEffect (синхронно, ДО отрисовки)
// 3. Браузер отрисовывает пиксели
// 4. useEffect (асинхронно, ПОСЛЕ отрисовки)

Графическое представление

┌─────────────────────────────────────────┐
│ Компонент рендерится (синхронно)        │
│ console.log('Render')                   │
└─────────────────────────────────────────┘
              ↓
┌─────────────────────────────────────────┐
│ DOM обновляется                          │
└─────────────────────────────────────────┘
              ↓
┌─────────────────────────────────────────┐
│ useLayoutEffect выполняется (синхронно) │
│ Может произойти еще один рендер          │
└─────────────────────────────────────────┘
              ↓
┌─────────────────────────────────────────┐
│ Браузер отрисовывает на экран           │
└─────────────────────────────────────────┘
              ↓
┌─────────────────────────────────────────┐
│ useEffect выполняется (асинхронно)      │
└─────────────────────────────────────────┘

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

Правильно: Загрузка данных в useEffect

function UserProfile({ userId }) {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)
  
  useEffect(() => {
    setLoading(true)
    fetch(`/api/users/${userId}`)
      .then(response => response.json())
      .then(data => {
        setUser(data)
        setLoading(false)
      })
      .catch(err => {
        setError(err)
        setLoading(false)
      })
  }, [userId])
  
  if (loading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>
  return <div>User: {user.name}</div>
}

Правильно: Очистка слушателей события

function WindowResize() {
  const [width, setWidth] = useState(window.innerWidth)
  
  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth)
    
    // Добавляем слушатель
    window.addEventListener('resize', handleResize)
    
    // Очищаем при размонтировании или изменении зависимостей
    return () => {
      window.removeEventListener('resize', handleResize)
    }
  }, [])
  
  return <div>Width: {width}</div>
}

Неправильно: useEffect с пустым массивом зависимостей

function BadExample() {
  // НЕПРАВИЛЬНО - dependency array важен
  useEffect(() => {
    console.log('Effect')  // Выполнится один раз при монтировании
  })  // ❌ Забыли массив зависимостей
  
  // ПРАВИЛЬНО
  useEffect(() => {
    console.log('Effect')
  }, [])  // ✅ Выполнится один раз
}

Race condition в useEffect

function SearchUsers({ query }) {
  const [results, setResults] = useState([])
  
  useEffect(() => {
    // Поиск асинхронный
    fetch(`/api/search?q=${query}`)
      .then(response => response.json())
      .then(data => setResults(data))
  }, [query])
  
  return (
    <ul>
      {results.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  )
}

// Если пользователь быстро меняет query:
// 1. Поиск по 'react' - запрос отправлен
// 2. Поиск по 'react-router' - запрос отправлен
// 3. Ответ по 'react' приходит позже
// 4. Результаты обновляются на 'react' (неправильно!)

Решение с AbortController:

function SearchUsers({ query }) {
  const [results, setResults] = useState([])
  
  useEffect(() => {
    const controller = new AbortController()
    
    fetch(`/api/search?q=${query}`, { signal: controller.signal })
      .then(response => response.json())
      .then(data => setResults(data))
      .catch(err => {
        if (err.name !== 'AbortError') {
          console.error(err)
        }
      })
    
    return () => controller.abort()  // Отменяем при новом запросе
  }, [query])
  
  return (
    <ul>
      {results.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  )
}

Таблица сравнения

┌──────────────┬─────────────────────┬────────────────────┐
│ Свойство     │ useEffect           │ useLayoutEffect    │
├──────────────┼─────────────────────┼────────────────────┤
│ Выполнение   │ Асинхронно          │ Синхронно          │
│ Когда        │ После рендера       │ После рендера,     │
│              │ и отрисовки         │ перед отрисовкой   │
│ Использование│ Данные, события     │ Измерения, layout  │
│ Производство │ Безопасен           │ Может заблокировать│
└──────────────┴─────────────────────┴────────────────────┘

Вывод

useEffect выполняется асинхронно:

  1. Компонент рендерится синхронно
  2. DOM обновляется
  3. useEffect запускается асинхронно (в конце цикла рендера)
  4. Браузер отрисовывает результат

Это сделано для производительности - асинхронное выполнение не блокирует отрисовку. Используй useLayoutEffect только если действительно нужно выполнить что-то синхронно перед отрисовкой.