Побочные эффекты и получение данных — useEffect

В этой главе вы узнаете о самом важном хуке побочных эффектов в React — useEffect, и научитесь использовать fetch, чтобы загружать данные блога из JSON-файла в нужный момент.


Что такое побочный эффект?

В Reactпобочный эффектотносится к операциям, не связанным с рендерингом:

  • Запрос данных с сервера
  • Изменение localStorage
  • Установка таймеров с помощью setInterval
  • Ручное изменение DOM
  • Подписка на внешние события

Побочные эффекты нельзя выполнять напрямую в теле функции компонента (это приводило бы к их выполнению при каждом рендеринге); их необходимо помещать вuseEffect.


Базовое использование useEffect

Пример

import { useState, useEffect } from 'react'

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: Создайте файл данных

Пример

// Путь к файлу: public/posts.json
[
  {
    "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

Пример

// Путь к файлу: src/pages/HomePage.jsx
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 может возвращать функцию, известную какфункция очистки。

Она выполняется в два момента:Когда компонент размонтируетсяиПеред выполнением следующего эффекта。

Пример

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Анимация загрузки, скелетон-экран
УспехДанные полученыОбычный вывод содержимого
Failureerror !== nullСообщение об ошибке + кнопка повтора
Пустые данныеДанные — пустой массивСообщение о пустом состоянии

Сравнение с жизненным циклом Vue3

Vue3ReactОписание
onMounteduseEffect(fn, [])Выполняется после первоначального рендера
watch(keyword, fn)useEffect(fn, [keyword])Реагирует на определённые изменения данных
onBeforeUnmountuseEffect 的 cleanupОчистка перед уничтожением компонента
Нет прямого аналогаuseEffect(() => { fn; return cleanup }, [dep])Когда dep изменяется, сначала выполняется очистка, затем fn

Краткое содержание главы

В этой главе вы освоили основное использование useEffect: три способа записи массива зависимостей, асинхронную загрузку данных с помощью fetch, обработку четырёх состояний UI и роль функции очистки.

Теперь данные блога динамически загружаются из внешнего JSON-файла, а не жёстко закодированы в JS.

Другие расширения