Побочные эффекты и получение данных — useEffect
В этой главе вы узнаете о самом важном хуке побочных эффектов в React — useEffect, и научитесь использовать fetch, чтобы загружать данные блога из JSON-файла в нужный момент.
Что такое побочный эффект?
В Reactпобочный эффектотносится к операциям, не связанным с рендерингом:
- Запрос данных с сервера
- Изменение localStorage
- Установка таймеров с помощью setInterval
- Ручное изменение DOM
- Подписка на внешние события
Побочные эффекты нельзя выполнять напрямую в теле функции компонента (это приводило бы к их выполнению при каждом рендеринге); их необходимо помещать вuseEffect.
Базовое использование useEffect
Пример
function HomePage() {
const [message, setMessage] = useState('Загрузка...')
// useEffect(fn, deps): выполняет побочный эффект после рендеринга компонента
useEffect(() => {
console.log('Компонент смонтирован в DOM; побочные эффекты теперь можно выполнять.')
setMessage('Загрузка данных завершена!')
}, []) // Пустой массив зависимостей = выполняется только один раз после первоначального рендеринга
return <p>{message}</p>
}
Три способа записать массив зависимостей
| Синтаксис | Время выполнения | Сценарий использования |
|---|---|---|
useEffect(fn) | Выполняется после каждого рендеринга | Используется редко; может приводить к бесконечным циклам |
useEffect(fn, []) | Выполняется только один раз после первоначального рендеринга | Загрузка данных, инициализация сторонних библиотек |
useEffect(fn, [dep]) | Запускается после первоначального рендера + при изменении dep | Отслеживайте изменения конкретных данных и выполняйте соответствующие операции |
Разница между пропуском массива зависимостей и пустым массивом:
useEffect(fn)Запускается после каждого рендера;useEffect(fn, [])Он запускается только один раз после первоначального рендера. Первый вариант легко может вызвать бесконечные циклы (setState в effect → вызывает рендер → effect запускается снова → ...), поэтому его почти никогда не используют.
Загрузка данных JSON с помощью fetch
Как и в Vue3, мы помещаем данные статьи вpublic/posts.jsonфайл.
Шаг 1: Создайте файл данных
Пример
[
{
"id": 1,
"title": "Полное руководство по началу работы с React",
"summary": "Изучите React Hooks с нуля, охватывая основные концепции, такие как useState и useEffect.",
"content": "<h2>Зачем изучать React?</h2><p>React — один из самых популярных фронтенд-фреймворков...</p>",
"category": "React",
"date": "2024-05-10"
},
{
"id": 2,
"title": "Асинхронное программирование в JavaScript: объяснение",
"summary": "Разберитесь с Promise, async/await, событийным циклом и очередью микрозадач в одной статье.",
"content": "<h2>Что такое асинхронность?</h2><p>JS однопоточный...</p>",
"category": "JavaScript",
"date": "2024-05-08"
},
{
"id": 3,
"title": "CSS Grid Layout на практике",
"summary": "Легко реализуйте сложные адаптивные макеты с помощью CSS Grid.",
"content": "<h2>Начало работы с Grid</h2><p>Grid — это двумерная система компоновки...</p>",
"category": "CSS",
"date": "2024-05-05"
}
]
Шаг 2: Загрузка данных в useEffect
Пример
import { useState, useEffect, useMemo } from 'react'
import BlogCard from '../components/BlogCard'
import CategoryFilter from '../components/CategoryFilter'
function HomePage() {
const [articles, setArticles] = useState([]) // Данные статьи
const [isLoading, setIsLoading] = useState(true) // Состояние загрузки
const [error, setError] = useState(null) // Сообщение об ошибке
const [activeCategory, setActiveCategory] = useState('Все')
// useEffect загружает данные: пустой массив зависимостей = выполняется только один раз после начального рендера
useEffect(() => {
let cancelled = false // Флаг для предотвращения setState после размонтирования компонента
async function fetchPosts() {
setIsLoading(true)
setError(null)
try {
const res = await fetch('/posts.json')
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
if (!cancelled) {
setArticles(data)
}
} catch (err) {
if (!cancelled) {
setError(err.message)
}
} finally {
if (!cancelled) {
setIsLoading(false)
}
}
}
fetchPosts()
// Функция очистки: установить cancelled в true при размонтировании компонента
return () => { cancelled = true }
}, [])
const categories = useMemo(() => {
return ['Все', ...new Set(articles.map(a => a.category))]
}, [articles])
const filteredArticles = useMemo(() => {
if (activeCategory === 'All') return articles
return articles.filter(a => a.category === activeCategory)
}, [articles, activeCategory])
return (
<div>
<h2 className="section-title">Последние статьи</h2>
{/* Загрузка */}
{isLoading && <p className="status-msg">Загрузка, пожалуйста, подождите...</p>}
{/* Ошибка загрузки */}
{error && (
<div className="status-msg error">
<p>Не удалось загрузить:{error}</p>
<button onClick={() => window.location.reload()}>Повторить</button>
</div>
)}
{/* Загрузка данных завершена */}
{!isLoading && !error && (
<>
<CategoryFilter
categories={categories}
activeCategory={activeCategory}
onCategoryChange={setActiveCategory}
/>
<p className="result-info">Всего{filteredArticles.length}статей</p>
{filteredArticles.length === 0 ? (
<p className="empty-tip">В этой категории пока нет статей</p>
) : (
<div className="article-grid">
{filteredArticles.map(article => (
<BlogCard key={article.id} {...article} />
))}
</div>
)}
</>
)}
</div>
)
}
export default HomePage
cancelledФлаг — распространённый шаблон в React. Когда fetch ещё выполняется, а пользователь покидает страницу (компонент размонтируется), последующий setState выдаст предупреждение об ошибке. Установка его в функции очисткиcancelled = trueпозволяет пропустить setState после размонтирования.
Функция очистки
Функция обратного вызова useEffect может возвращать функцию, известную какфункция очистки。
Она выполняется в два момента:Когда компонент размонтируетсяиПеред выполнением следующего эффекта。
Пример
// Побочный эффект: подписка на события
const handleScroll = () => console.log('Прокручено')
window.addEventListener('scroll', handleScroll)
// Вернуть функцию очистки
return () => {
// Автоматически удалить обработчик события при размонтировании компонента, чтобы предотвратить утечки памяти
window.removeEventListener('scroll', handleScroll)
}
}, [])
useEffect(() => {
// Побочный эффект: установить таймер
const timer = setInterval(() => {
console.log('Выполняется каждую секунду')
}, 1000)
return () => clearInterval(timer) // Очистить таймер
}, [])
| Побочный эффект | Требуется очистка? | Метод очистки |
|---|---|---|
| fetch-запрос | Да | Используйте флаг cancelled или AbortController |
| Обработчик событий | Да | removeEventListener |
| Таймер | Да | clearInterval / clearTimeout |
| Изменить DOM, затем восстановить | Да | Восстановите исходное состояние в функции очистки |
| Запись в localStorage | нет | Очистка не требуется |
Четыре состояния обработки асинхронных запросов
Любой запрос данных должен охватывать следующие четыре состояния UI:
| State | Условие | UI |
|---|---|---|
| Загрузка | isLoading === true | Анимация загрузки, скелетон-экран |
| Успех | Данные получены | Обычный вывод содержимого |
| Failure | error !== null | Сообщение об ошибке + кнопка повтора |
| Пустые данные | Данные — пустой массив | Сообщение о пустом состоянии |
Сравнение с жизненным циклом Vue3
| Vue3 | React | Описание |
|---|---|---|
onMounted | useEffect(fn, []) | Выполняется после первоначального рендера |
watch(keyword, fn) | useEffect(fn, [keyword]) | Реагирует на определённые изменения данных |
onBeforeUnmount | useEffect 的 cleanup | Очистка перед уничтожением компонента |
| Нет прямого аналога | useEffect(() => { fn; return cleanup }, [dep]) | Когда dep изменяется, сначала выполняется очистка, затем fn |
Краткое содержание главы
В этой главе вы освоили основное использование useEffect: три способа записи массива зависимостей, асинхронную загрузку данных с помощью fetch, обработку четырёх состояний UI и роль функции очистки.
Теперь данные блога динамически загружаются из внешнего JSON-файла, а не жёстко закодированы в JS.
Другие расширения