Вивчай

useEffect

React useEffect — ряд доміно що падають ланцюговою реакцією на дерев'яному століReact useEffect — ряд доміно що падають ланцюговою реакцією на дерев'яному столі

У попередніх уроках ми працювали зі state та подіями. Але що, якщо потрібно завантажити дані з API при відкритті сторінки? Або підписатись на подію вікна? Або запустити таймер? Все це -- побічні ефекти (side effects), і для них існує хук useEffect.


Що таке побічні ефекти?

Компонент React має одну основну задачу -- повернути JSX на основі props та state. Все інше -- побічні ефекти:

  • Завантаження даних з API (fetch)
  • Підписка на події (addEventListener)
  • Таймери (setTimeout, setInterval)
  • Зміна document.title
  • Робота з localStorage
  • Фокусування елемента

Синтаксис useEffect

import { useEffect } from 'react'

useEffect(() => {
  // Код ефекту (виконується після рендеру)
}, [/* масив залежностей */])

Приклад: зміна document.title

import { useState, useEffect } from 'react'

function Counter() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    document.title = `Лічильник: ${count}`
  }, [count])

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(prev => prev + 1)}>+1</button>
    </div>
  )
}

При кожній зміні count React оновлює заголовок вкладки браузера.


Масив залежностей (dependencies)

Другий аргумент useEffect визначає, коли ефект виконується:

1. Порожній масив [] -- тільки при першому рендері

useEffect(() => {
  console.log('Компонент змонтовано!')
  // Виконається один раз при появі компонента
}, [])

Це аналог componentDidMount з класових компонентів. Ідеально для початкового завантаження даних.

2. З залежностями [dep1, dep2] -- при зміні залежностей

useEffect(() => {
  console.log(`Ім'я змінилось на: ${name}`)
  // Виконається при першому рендері та при кожній зміні name
}, [name])

3. Без масиву -- після кожного рендеру

useEffect(() => {
  console.log('Компонент перемальовано')
  // Виконується після КОЖНОГО рендеру -- зазвичай так не потрібно!
})
Увага

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


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

Найчастіший випадок використання useEffect -- fetch даних:

import { useState, useEffect } from 'react'

interface User {
  id: number
  name: string
  email: string
}

function UserList() {
  const [users, setUsers] = useState<User[]>([])
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    async function fetchUsers() {
      try {
        const response = await fetch('https://jsonplaceholder.typicode.com/users')

        if (!response.ok) {
          throw new Error(`HTTP помилка: ${response.status}`)
        }

        const data: User[] = await response.json()
        setUsers(data)
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Невідома помилка')
      } finally {
        setLoading(false)
      }
    }

    fetchUsers()
  }, []) // [] -- завантажити один раз при монтуванні

  if (loading) return <p>Завантаження...</p>
  if (error) return <p className="error">Помилка: {error}</p>

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name} — {user.email}</li>
      ))}
    </ul>
  )
}
Інфо

Зверни увагу: ми створюємо async функцію всередині useEffect та одразу викликаємо її. Сам callback useEffect не може бути async -- React очікує, що він повертає або undefined, або cleanup-функцію.

Завантаження при зміні параметра

function UserPosts({ userId }: { userId: number }) {
  const [posts, setPosts] = useState<Post[]>([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    setLoading(true)

    async function fetchPosts() {
      try {
        const res = await fetch(
          `https://jsonplaceholder.typicode.com/users/${userId}/posts`
        )
        const data = await res.json()
        setPosts(data)
      } catch (err) {
        console.error('Помилка:', err)
      } finally {
        setLoading(false)
      }
    }

    fetchPosts()
  }, [userId]) // Перезавантажити при зміні userId

  // ...рендер
}

Cleanup function -- прибирання

useEffect може повертати функцію, яка виконається при розмонтуванні компонента або перед наступним запуском ефекту:

useEffect(() => {
  // Setup
  console.log('Ефект запущено')

  return () => {
    // Cleanup -- виконається при розмонтуванні або перед наступним запуском
    console.log('Прибирання')
  }
}, [dependency])

Приклад: підписка на подію

function WindowSize() {
  const [width, setWidth] = useState(window.innerWidth)

  useEffect(() => {
    function handleResize() {
      setWidth(window.innerWidth)
    }

    window.addEventListener('resize', handleResize)

    // Cleanup -- прибираємо підписку при розмонтуванні
    return () => {
      window.removeEventListener('resize', handleResize)
    }
  }, [])

  return <p>Ширина вікна: {width}px</p>
}

Приклад: таймер

function Timer() {
  const [seconds, setSeconds] = useState(0)

  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(prev => prev + 1)
    }, 1000)

    // Cleanup -- зупиняємо таймер при розмонтуванні
    return () => clearInterval(interval)
  }, [])

  return <p>Секунд: {seconds}</p>
}
Увага

Якщо ти підписуєшся на щось (addEventListener, setInterval, WebSocket) -- завжди повертай cleanup-функцію. Без цього підписки "витікають" -- компонент зникає, а обробники продовжують працювати, споживаючи пам'ять.


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

1. Нескінченний цикл

// ПОМИЛКА: setUsers змінює state → рендер → useEffect → setUsers → рендер → ...
useEffect(() => {
  fetch('/api/users')
    .then(res => res.json())
    .then(data => setUsers(data))
}) // Забули масив залежностей!

// ВИПРАВЛЕННЯ:
useEffect(() => {
  fetch('/api/users')
    .then(res => res.json())
    .then(data => setUsers(data))
}, []) // Додали порожній масив

2. Забута залежність

// ПОМИЛКА: ефект "бачить" старе значення searchTerm
useEffect(() => {
  fetch(`/api/search?q=${searchTerm}`)
    .then(res => res.json())
    .then(data => setResults(data))
}, []) // searchTerm не в залежностях!

// ВИПРАВЛЕННЯ:
useEffect(() => {
  fetch(`/api/search?q=${searchTerm}`)
    .then(res => res.json())
    .then(data => setResults(data))
}, [searchTerm]) // Додали searchTerm

3. Race condition при fetch

Якщо userId змінюється швидко, відповідь старого запиту може прийти після нового:

useEffect(() => {
  let cancelled = false

  async function fetchData() {
    const res = await fetch(`/api/users/${userId}`)
    const data = await res.json()

    if (!cancelled) {
      setUser(data) // Тільки якщо запит не скасований
    }
  }

  fetchData()

  return () => {
    cancelled = true // Скасовуємо попередній запит
  }
}, [userId])

Підсумок

  • useEffect -- хук для побічних ефектів (fetch, таймери, підписки)
  • Масив залежностей: [] -- один раз, [dep] -- при зміні dep
  • Cleanup function -- повертаємо з useEffect для прибирання (видалення підписок, таймерів)
  • async/await: створюємо async-функцію всередині useEffect
  • Завжди вказуй масив залежностей
  • Використовуй cleanup для запобігання memory leaks та race conditions

Що далі?

У наступному уроці розберемо ще три корисні хуки: useRef для роботи з DOM, useMemo та useCallback для оптимізації продуктивності.

Інфо

Корисні посилання: