Синхронізація з ефектами: хук useEffect та функції очищення
Синхронізація з ефектами: хук useEffect та функції очищення
🔗 Офіційний референс: Синхронізація з ефектами (Synchronizing with Effects)
🎯 Мета лекції
- Зрозуміти різницю між рендерінгом UI та побічними ефектами.
- Опанувати синтаксис хука
useEffectта його три форми з різними масивами залежностей. - Навчитися писати функції очищення для запобігання витокам пам'яті.
- Застосувати TypeScript для безпечної типізації асинхронних ефектів та підписок.
🔑 Ключові терміни
- Ефект (Effect): побічна дія, що синхронізує компонент із зовнішньою системою (API, таймер, браузерна подія).
- Функція очищення (Cleanup Function): функція, яку повертає ефект для скасування підписок, таймерів та з'єднань.
- Масив залежностей (Dependency Array): список реактивних значень, при зміні яких ефект перезапускається.
- Strict Mode: режим розробки React, що навмисно подвійно викликає ефекти для виявлення проблем очищення.
Рендеринг проти побічних ефектів: фундаментальна різниця
До цього моменту ми працювали виключно з чистою логікою рендерингу: компоненти приймали пропси, обчислювали стан і повертали JSX-розмітку. Це була математично передбачувана система: одні й ті самі вхідні дані завжди давали однаковий результат без жодних змін зовнішнього світу.
Проте реальні застосунки повинні взаємодіяти з речами за межами React:
- Завантажувати дані з мережевого API.
- Підключатися до WebSocket-серверів для отримання оновлень у реальному часі.
- Змінювати заголовок документа
document.title. - Відстежувати події браузера (
scroll,resize,keydown). - Інтегруватися з картографічними бібліотеками, відеоплеєрами, графічними canvas.
Усі ці операції є побічними ефектами (side effects) — діями, що змінюють щось за межами обчислення розмітки. У React є чітке правило: побічні ефекти не повинні виконуватися під час рендерингу. Замість цього вони ізолюються у спеціальний хук useEffect, який виконується після того, як зміни вже застосовані до DOM.
useEffect часто вводить початківців в оману — здається, що це «хук для всього підряд». Насправді його точніше назвати «хуком синхронізації з зовнішніми системами» (synchronization hook). Якщо ваш код не виходить за межі React (наприклад, просто обчислює похідні дані з state), useEffect не потрібен.Анатомія useEffect: синтаксис та три форми масиву залежностей
Хук useEffect приймає два аргументи:
- Функцію ефекту (setup function) — код, що виконає побічну дію.
- Масив залежностей (dependency array) — список реактивних значень, при зміні яких ефект перезапускається.
Загальна сигнатура у TypeScript:
useEffect(
() => {
// Код побічної дії (setup)
return () => {
// Опціональна функція очищення (cleanup)
};
},
[dep1, dep2, ...] // Масив залежностей
);
Існує три фундаментальні форми useEffect залежно від масиву залежностей:
| Форма | Коли виконується | Приклад використання |
|---|---|---|
useEffect(() => { ... }) | Після кожного рендеру | Журналювання кожної зміни (рідко потрібно) |
useEffect(() => { ... }, []) | Один раз при монтуванні | Початкове завантаження даних, підписки |
useEffect(() => { ... }, [a, b]) | При зміні a або b | Синхронізація з prop/state |
useEffect(() => {...}) без другого аргументу) створює ефект, що виконується після кожного рендеру. Це призводить до каскадних оновлень та зациклень. Завжди явно вказуйте масив залежностей, навіть якщо він порожній.Перший ефект: змінюємо заголовок документа
Почнемо з найпростішого прикладу — зміни заголовка браузерної вкладки залежно від стану компонента:
import { useState, useEffect } from 'react'
export function DocumentTitleExample() {
const [count, setCount] = useState<number>(0)
// Ефект синхронізує заголовок документа зі станом count
useEffect(() => {
document.title = `Ви натиснули ${count} разів`
}, [count]) // Перезапускається щоразу, коли count змінюється
return (
<div>
<p>Відкрийте заголовок вкладки браузера — він змінюється!</p>
<p>Поточне значення: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount(0)}>Скинути</button>
</div>
)
}
Збирання React застосунку…
Що тут відбувається:
- React рендерить компонент і оновлює DOM.
- Браузер малює зміни на екрані (користувач бачить нове значення
count). - Після малювання React викликає функцію ефекту, яка змінює
document.title. - Коли
countзмінюється знову, React повторює цикл.
Ключовий момент: ефект виконується асинхронно після рендерингу, а не під час нього. Це гарантує, що користувач миттєво бачить оновлений інтерфейс, навіть якщо побічна дія займає час.
Ефект із порожнім масивом залежностей: виконання один раз при монтуванні
Якщо передати порожній масив [] як другий аргумент, ефект виконається лише один раз — коли компонент вперше з'явиться на екрані. Це ідеально для:
- Початкового завантаження даних з API.
- Підписки на глобальні події (
window.resize,document.keydown). - Ініціалізації сторонніх бібліотек (карти, відеоплеєри).
import { useState, useEffect } from 'react'
interface User {
id: number
name: string
email: string
}
export function UserProfileLoader() {
const [user, setUser] = useState<User | null>(null)
const [isLoading, setIsLoading] = useState<boolean>(true)
useEffect(() => {
// Симулюємо мережевий запит
const fetchUser = async () => {
setIsLoading(true)
// У реальному застосунку тут був би fetch('/api/user')
await new Promise((resolve) => setTimeout(resolve, 1000))
setUser({
id: 1,
name: 'Олександра Петренко',
email: 'oleksandra@example.com',
})
setIsLoading(false)
}
fetchUser()
}, []) // Порожній масив: виконується лише при монтуванні
if (isLoading) {
return <p>Завантаження профілю...</p>
}
if (!user) {
return <p>Помилка завантаження даних</p>
}
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
)
}
[] означає «жодне реактивне значення не впливає на цей ефект». React виконає його один раз після першого рендеру і більше ніколи не торкнеться, доки компонент існує на екрані.Функція очищення: запобігання витокам пам'яті та завислим підпискам
Багато побічних ефектів створюють ресурси, які потрібно звільнити: таймери, мережеві з'єднання, підписки на події, інтервали. Якщо не очистити їх належним чином, виникають витоки пам'яті (memory leaks) — застосунок споживає все більше ресурсів, поки не сповільниться або не зависне.
Функція очищення (cleanup function) — це функція, яку ви повертаєте з ефекту. React викликає її у двох випадках:
- Перед повторним виконанням ефекту (коли залежності змінилися).
- При остаточному розмонтуванні компонента (видаленні з DOM).
Приклад: таймер із автоматичним очищенням
Створимо секундомір, що оновлюється кожну секунду:
import { useState, useEffect } from 'react'
export function LiveClock() {
const [time, setTime] = useState<Date>(new Date())
useEffect(() => {
console.log('⏰ Запуск таймера')
// Створюємо інтервал, що оновлює час кожну секунду
const intervalId = setInterval(() => {
setTime(new Date())
}, 1000)
// Функція очищення: зупиняємо інтервал
return () => {
console.log('🛑 Очищення таймера')
clearInterval(intervalId)
}
}, []) // Порожній масив: створюємо інтервал лише один раз
return (
<div>
<h3>Поточний час</h3>
<p style={{ fontSize: '2rem', fontWeight: 'bold' }}>{time.toLocaleTimeString('uk-UA')}</p>
</div>
)
}
Збирання React застосунку…
Що відбувається при схованні компонента:
- Користувач натискає кнопку «Сховати годинник».
- React видаляє компонент
LiveClockз DOM (розмонтування). - Перед розмонтуванням React викликає функцію очищення:
clearInterval(intervalId). - Таймер зупиняється — більше не викликається
setTimeна неіснуючому компоненті.
setInterval продовжуватиме працювати навіть після видалення компонента. Це призведе до спроб оновлення стану на розмонтованому компоненті — React видасть попередження у консолі: "Can't perform a React state update on an unmounted component".Strict Mode та подвійне виконання ефектів у розробці
У файлі main.tsx (або index.tsx) ваш застосунок, ймовірно, обгорнутий у <React.StrictMode>:
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
У режимі розробки (development) Strict Mode навмисно викликає кожен ефект двічі поспіль:
- Перший цикл:
mount→ виконується ефект →cleanup→unmount - Другий цикл:
mount→ виконується ефект знову
Це схоже на те, ніби React швидко схований і показав компонент заново. Мета — виявити помилки, коли ваша функція очищення не нейтралізує побічну дію належним чином.
Якщо у консолі ви бачите повідомлення ⏰ Запуск таймера двічі поспіль — це нормально і відбувається лише в development. У production-збірці подвійне виконання автоматично відключається.
Підписка на події браузера: відстеження розміру вікна
Коли компонент повинен реагувати на події, що відбуваються за межами React (прокручування, зміна розміру вікна, натискання клавіш), використовується підписка через addEventListener. Обов'язково видаляйте слухача у функції очищення через removeEventListener.
import { useState, useEffect } from 'react'
interface WindowSize {
width: number
height: number
}
export function WindowSizeTracker() {
const [windowSize, setWindowSize] = useState<WindowSize>({
width: window.innerWidth,
height: window.innerHeight,
})
useEffect(() => {
const handleResize = (): void => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
})
}
// Підписуємося на подію зміни розміру вікна
window.addEventListener('resize', handleResize)
// Функція очищення: видаляємо слухача
return () => {
window.removeEventListener('resize', handleResize)
}
}, []) // Порожній масив: підписка створюється один раз
return (
<div>
<h3>Розмір вікна браузера</h3>
<p>Ширина: {windowSize.width}px</p>
<p>Висота: {windowSize.height}px</p>
<p style={{ fontSize: '0.875rem', color: '#64748b' }}>Спробуйте змінити розмір вікна браузера</p>
</div>
)
}
Збирання React застосунку…
Ключові моменти:
- Функція
handleResizeоголошена всерединіuseEffect, тому вона має доступ доsetWindowSizeчерез замикання. addEventListenerтаremoveEventListenerотримують точно ту саму функцію — React видалить саме той слухач, що був доданий.- Якщо забути
removeEventListener, кожен повторний рендер додаватиме новий слухач, створюючи витік пам'яті.
Асинхронні ефекти: завантаження даних із мережі
Однією з найпоширеніших задач useEffect є завантаження даних із API. Проте тут виникає підступна проблема: функція ефекту не може бути async. Це обмеження React — ефект повинен повертати або undefined, або функцію очищення, а не Promise.
Рішення: оголосити асинхронну функцію всередині ефекту та викликати її одразу.
import { useState, useEffect } from 'react'
interface Post {
id: number
title: string
body: string
}
export function PostLoader({ postId }: { postId: number }) {
const [post, setPost] = useState<Post | null>(null)
const [isLoading, setIsLoading] = useState<boolean>(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
// Оголошуємо async функцію всередині ефекту
const fetchPost = async (): Promise<void> => {
setIsLoading(true)
setError(null)
try {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`)
if (!response.ok) {
throw new Error(`HTTP помилка: ${response.status}`)
}
const data: Post = await response.json()
setPost(data)
} catch (err) {
setError(err instanceof Error ? err.message : 'Невідома помилка')
} finally {
setIsLoading(false)
}
}
// Викликаємо функцію одразу
fetchPost()
}, [postId]) // Перезавантажуємо дані при зміні postId
if (isLoading) {
return <p>Завантаження публікації...</p>
}
if (error) {
return <p style={{ color: '#ef4444' }}>Помилка: {error}</p>
}
if (!post) {
return <p>Публікацію не знайдено</p>
}
return (
<article>
<h3>{post.title}</h3>
<p>{post.body}</p>
</article>
)
}
Збирання React застосунку…
postId (наприклад, клікає 1 → 2 → 3), React запустить три асинхронні запити паралельно. Проте ми не можемо гарантувати, що відповіді прийдуть у тому ж порядку — публікація #3 може завантажитися раніше, ніж #2.Результат: користувач вибрав публікацію #3, але на екрані відображається #2 (остання відповідь, яка прийшла). Це класична проблема гонки даних (race condition). Рішення — додати функцію очищення з прапорцем ignore:useEffect(() => {
let ignore = false // Прапорець скасування
const fetchPost = async () => {
// ... виконуємо запит ...
if (!ignore) {
setPost(data) // Оновлюємо стан лише якщо не скасовано
}
}
fetchPost()
return () => {
ignore = true // При зміні postId скасовуємо попередній запит
}
}, [postId])
Ефект із залежностями: синхронізація зі станом та пропсами
Коли ефект повинен перезапускатися у відповідь на зміну конкретних реактивних значень (стану або пропсів), ці значення вказуються у масиві залежностей. React порівнює їх із попередніми значеннями за допомогою Object.is() і перезапускає ефект, якщо хоча б одне змінилося.
import { useState, useEffect } from 'react'
export function SearchLogger({ query }: { query: string }) {
const [searchCount, setSearchCount] = useState<number>(0)
useEffect(() => {
if (query.trim() !== '') {
console.log(`Пошуковий запит: "${query}"`)
setSearchCount((prev) => prev + 1)
}
}, [query]) // Ефект залежить від пропса query
return (
<div>
<p>
Поточний запит: <strong>{query || '(порожньо)'}</strong>
</p>
<p>Кількість пошуків: {searchCount}</p>
</div>
)
}
export function SearchDemo() {
const [inputValue, setInputValue] = useState<string>('')
return (
<div>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Введіть пошуковий запит..."
/>
<SearchLogger query={inputValue} />
</div>
)
}
Кожного разу, коли значення query змінюється, React:
- Викликає функцію очищення попереднього ефекту (якщо вона існує).
- Виконує нову версію ефекту з актуальним значенням
query.
react-hooks/exhaustive-deps допоможе виявити забуті залежності автоматично.Типові помилки та антипатерни useEffect
1. Забута функція очищення для таймерів
// ❌ Погано: інтервал ніколи не зупиняється
useEffect(() => {
setInterval(() => {
console.log('Tick')
}, 1000)
}, [])
// ✅ Правильно: інтервал очищається при розмонтуванні
useEffect(() => {
const id = setInterval(() => {
console.log('Tick')
}, 1000)
return () => clearInterval(id)
}, [])
2. Відсутність масиву залежностей
// ❌ Погано: ефект виконується після кожного рендеру
useEffect(() => {
fetchData()
}) // Масив залежностей відсутній!
// ✅ Правильно: явно вказуємо порожній масив або залежності
useEffect(() => {
fetchData()
}, [])
3. Неправильна залежність: об'єкт або масив
// ❌ Погано: новий об'єкт створюється при кожному рендері
const options = { page: 1, limit: 10 }
useEffect(() => {
fetchData(options)
}, [options]) // options — це щоразу новий об'єкт!
// ✅ Правильно: передавайте примітивні значення або useMemo
useEffect(() => {
fetchData({ page: 1, limit: 10 })
}, []) // Або витягніть page і limit окремо у залежності
4. Async функція як тіло ефекту
// ❌ Погано: useEffect не може повертати Promise
useEffect(async () => {
const data = await fetchData()
setData(data)
}, [])
// ✅ Правильно: оголосіть async функцію всередині
useEffect(() => {
const loadData = async () => {
const data = await fetchData()
setData(data)
}
loadData()
}, [])
Практичні завдання
Створіть компонент OnlineStatus, який відстежує стан інтернет-з'єднання користувача за допомогою подій online та offline об'єкта window. Компонент повинен показувати індикатор кольору (зелений для онлайн, червоний для офлайн) та текстове повідомлення.
Вимоги:
- Використовуйте
useEffectдля підписки на подіїwindow.addEventListener('online', ...)таwindow.addEventListener('offline', ...). - Початковий стан визначте через
navigator.onLine. - Обов'язково видаліть слухачів події у функції очищення.
- Типізуйте стан через TypeScript:
useState<boolean>.
Підказка:
const [isOnline, setIsOnline] = useState<boolean>(navigator.onLine)
import { useState, useEffect } from 'react'
export function OnlineStatus() {
const [isOnline, setIsOnline] = useState<boolean>(navigator.onLine)
useEffect(() => {
const handleOnline = (): void => {
setIsOnline(true)
}
const handleOffline = (): void => {
setIsOnline(false)
}
window.addEventListener('online', handleOnline)
window.addEventListener('offline', handleOffline)
// Функція очищення: видаляємо обидва слухачі
return () => {
window.removeEventListener('online', handleOnline)
window.removeEventListener('offline', handleOffline)
}
}, []) // Порожній масив: підписка створюється один раз
return (
<div
style={{
padding: '16px',
borderRadius: '8px',
backgroundColor: isOnline ? '#dcfce7' : '#fee2e2',
color: isOnline ? '#166534' : '#991b1b',
textAlign: 'center',
fontWeight: '600',
}}
>
<span
style={{
display: 'inline-block',
width: '12px',
height: '12px',
borderRadius: '50%',
backgroundColor: isOnline ? '#22c55e' : '#ef4444',
marginRight: '8px',
}}
/>
{isOnline ? "Онлайн — з'єднання активне" : 'Офлайн — перевірте інтернет'}
</div>
)
}
Покрокове пояснення:
- Початковий стан:
navigator.onLineповертає булеве значення, що відображає поточний статус з'єднання при монтуванні компонента. - Підписка на події:
window.addEventListenerдодає слухачів для подійonlineтаoffline, які спрацьовують при зміні мережевого стану. - Оновлення стану: Коли браузер виявляє зміну з'єднання, викликається відповідний обробник, що оновлює
isOnline. - Очищення:
removeEventListenerу функції очищення видаляє слухачів при розмонтуванні, запобігаючи витокам пам'яті.
Створіть компонент CountdownTimer, який приймає проп seconds (початкова кількість секунд) і відраховує час до нуля. Коли таймер досягає нуля, покажіть повідомлення «Час вийшов!» і зупиніть відлік.
Вимоги:
- Використовуйте
setIntervalдля зменшення лічильника кожну секунду. - Зупиніть інтервал, коли лічильник досягне
0. - При зміні пропса
secondsтаймер має перезапуститися з новим значенням. - Обов'язково очистіть інтервал у функції cleanup.
Інтерфейс компонента:
interface CountdownTimerProps {
seconds: number
}
import { useState, useEffect } from 'react'
interface CountdownTimerProps {
seconds: number
}
export function CountdownTimer({ seconds }: CountdownTimerProps) {
const [timeLeft, setTimeLeft] = useState<number>(seconds)
useEffect(() => {
// Скидаємо таймер при зміні пропса seconds
setTimeLeft(seconds)
// Якщо час уже вийшов, не створюємо інтервал
if (seconds <= 0) {
return
}
const intervalId = setInterval(() => {
setTimeLeft((prev) => {
// Якщо лічильник досяг 1, наступний крок — 0, зупиняємо інтервал
if (prev <= 1) {
clearInterval(intervalId)
return 0
}
return prev - 1
})
}, 1000)
// Функція очищення: зупиняємо інтервал при розмонтуванні або зміні seconds
return () => {
clearInterval(intervalId)
}
}, [seconds]) // Перезапускаємо ефект при зміні початкового значення
if (timeLeft === 0) {
return (
<div
style={{
padding: '24px',
borderRadius: '12px',
background: '#fef3c7',
color: '#92400e',
textAlign: 'center',
fontSize: '20px',
fontWeight: '700',
}}
>
⏰ Час вийшов!
</div>
)
}
return (
<div
style={{
padding: '24px',
borderRadius: '12px',
background: '#dbeafe',
color: '#1e40af',
textAlign: 'center',
}}
>
<div style={{ fontSize: '48px', fontWeight: '700', marginBottom: '8px' }}>{timeLeft}</div>
<div style={{ fontSize: '14px', color: '#64748b' }}>секунд до завершення</div>
</div>
)
}
// Приклад використання з кнопкою перезапуску
export function CountdownDemo() {
const [duration, setDuration] = useState<number>(10)
return (
<div>
<CountdownTimer seconds={duration} />
<button
onClick={() => setDuration(10)}
style={{
marginTop: '16px',
padding: '10px 20px',
border: 'none',
borderRadius: '8px',
background: '#3b82f6',
color: 'white',
fontWeight: '600',
cursor: 'pointer',
}}
>
Перезапустити
</button>
</div>
)
}
Покрокове пояснення:
- Ініціалізація:
timeLeftотримує початкове значення з пропсаseconds. - Ефект із залежністю: При зміні
secondsефект перезапускається, скидаючи таймер до нового значення. - Функціональне оновлення:
setTimeLeft((prev) => prev - 1)гарантує, що ми завжди працюємо з найсвіжішим значенням лічильника. - Умовна зупинка: Коли
prev <= 1, викликаєтьсяclearInterval(intervalId)усередині колбека, зупиняючи відлік. - Очищення: Функція cleanup запускається при розмонтуванні або зміні
seconds, видаляючи старий інтервал перед створенням нового.
clearInterval усередині setInterval технічно працює, але елегантніший підхід — використати додатковий useEffect, що спостерігає за timeLeft === 0 і зупиняє інтервал ззовні. Проте для навчальних цілей поточне рішення демонструє важливу концепцію: функція оновлення стану може містити побічну логіку.Створіть компонент PageTimer, який вимірює, скільки секунд користувач провів на поточній сторінці. Лічильник має починатися при монтуванні компонента і оновлюватися кожну секунду. При розмонтуванні компонента виведіть у консоль загальний час перебування.
Вимоги:
- Використовуйте
setIntervalдля оновлення лічильника кожну секунду. - У функції очищення виведіть
console.logіз загальним часом перебування. - Формат виводу:
"Користувач провів на сторінці: X секунд".
import { useState, useEffect, useRef } from 'react'
export function PageTimer() {
const [elapsedSeconds, setElapsedSeconds] = useState<number>(0)
const startTimeRef = useRef<number>(Date.now())
useEffect(() => {
const intervalId = setInterval(() => {
setElapsedSeconds((prev) => prev + 1)
}, 1000)
// Функція очищення: виводимо загальний час у консоль
return () => {
clearInterval(intervalId)
const totalTime = Math.floor((Date.now() - startTimeRef.current) / 1000)
console.log(`Користувач провів на сторінці: ${totalTime} секунд`)
}
}, []) // Порожній масив: ефект створюється один раз
const formatTime = (seconds: number): string => {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}
return (
<div
style={{
padding: '20px',
borderRadius: '12px',
background: '#f1f5f9',
textAlign: 'center',
}}
>
<div style={{ fontSize: '14px', color: '#64748b', marginBottom: '8px' }}>Час на сторінці</div>
<div style={{ fontSize: '36px', fontWeight: '700', color: '#0f172a' }}>{formatTime(elapsedSeconds)}</div>
</div>
)
}
Покрокове пояснення:
- Референс стартового часу:
useRefзберігає мітку часу монтування компонента без перерендерів. - Інтервал: Кожну секунду збільшуємо
elapsedSecondsна1. - Функція очищення: При розмонтуванні обчислюємо точний час через різницю
Date.now() - startTimeRef.current(це точніше, ніж простоelapsedSeconds, оскількиsetIntervalможе затримуватися). - Формат часу: Функція
formatTimeперетворює секунди у форматMM:SSдля зручності.
const [showTimer, setShowTimer] = useState(true)
return (
<div>
{showTimer && <PageTimer />}
<button onClick={() => setShowTimer(false)}>Сховати таймер</button>
</div>
)
Підсумок: коли та як використовувати useEffect
✅ Використовуйте useEffect для:
- Підписок на події:
window.addEventListener, WebSocket, тощо. - Мережевих запитів: завантаження даних з API при монтуванні або зміні параметрів.
- Синхронізації з браузерним API:
document.title,localStorage, геолокація. - Інтеграції сторонніх бібліотек: ініціалізація карт, графіків, відеоплеєрів.
- Таймерів та інтервалів:
setTimeout,setInterval.
❌ Не використовуйте useEffect для:
- Трансформації даних для рендерингу: обчислюйте похідні значення прямо під час рендеру або через
useMemo. - Обробки подій користувача: мережеві запити та зміни стану належать до обробників подій (
onClick,onSubmit). - Ініціалізації стану: використовуйте ліниву ініціалізацію
useState(() => computeInitial()). - Сповіщення батьківського компонента: викликайте колбеки з пропсів безпосередньо в обробниках подій.
useEffect:
Якщо ваш код не виходить за межі React (не торкається DOM, мережі, таймерів або сторонніх бібліотек), швидше за все, вам не потрібен useEffect. Спочатку спробуйте вирішити задачу через звичайну логіку рендерингу.Довідкова таблиця: три форми useEffect
| Синтаксис | Коли виконується setup | Коли виконується cleanup | Приклад використання |
|---|---|---|---|
useEffect(() => { ... }) | Після кожного рендеру | Перед кожним повторним виконанням і при розмонтуванні | Журналювання змін (рідко потрібно) |
useEffect(() => { ... }, []) | Один раз при монтуванні | Один раз при розмонтуванні | Початкове завантаження даних, підписки |
useEffect(() => { ... }, [a, b]) | При монтуванні та при зміні a або b | Перед повторним виконанням (із попередніми a, b) і при розмонтуванні | Синхронізація з пропсами/станом |
useEffect(() => {...})) створює ефект, що запускається після кожного рендеру, включаючи ті, які спричинені самим ефектом. Це призводить до нескінченних циклів оновлення.Використовуйте лінтер eslint-plugin-react-hooks із правилом exhaustive-deps, щоб автоматично виявляти забуті залежності.Наступні кроки
У цій лекції ви опанували фундаментальні принципи useEffect: синхронізацію з зовнішніми системами, функції очищення та масиви залежностей. Проте існують складніші сценарії та поширені антипатерни, які заслуговують на окреме вивчення:
- Наступна лекція: «Можливо, вам не потрібен ефект» — розглянемо типові помилки надмірного використання
useEffectта правильні альтернативи через обчислювані значення, обробники подій та ключkey. - Подальші теми: життєвий цикл реактивних ефектів, розділення подій та ефектів, оптимізація залежностей, кастомні хуки для інкапсуляції логіки ефектів.
- 01-react-effects-sample — анатомічний атлас патернів
useEffect. - 02-react-lifecycle-and-effects — колекція практичних UI-кейсів.
npm install && npm run dev), відкрийте консоль браузера та експериментуйте зі зміною залежностей, видаленням функцій очищення та перемиканням між компонентами для глибшого розуміння життєвого циклу ефектів.Context API: передавання даних без «пропс-дрілінгу»
Глибоке занурення в Context API для передавання даних через дерево компонентів без прокидання пропсів. Від простих тем до складних систем глобального стану.
Можливо, вам не потрібен ефект: антипатерни та альтернативи
Глибокий аналіз типових помилок надмірного використання useEffect та правильні альтернативи через обчислювані значення, обробники подій та ключ key. Навчіться писати швидший та простіший код.