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 для оптимізації продуктивності.
Корисні посилання:
- React.dev: Synchronizing with Effects -- повний гайд по useEffect
- React.dev: You Might Not Need an Effect -- коли useEffect НЕ потрібен
- React.dev: Lifecycle of Reactive Effects -- життєвий цикл ефектів