11 - Как работает хук useEffect? Где делать запрос на сервер?

Оценить качество материала и подачу материала автором видео:

Front-end

Трудоустройтесь middle front-end разработчиком на React JS (TypeScript) за 12-16 месяцев обучения с ежедневной менторской поддержкой в формате видео 1 на 1 и коммерческими проектами в портфолио

Перейти на курс
Front-end

Back-end

Трудоустройтесь middle back-end разработчиком за 12-16 месяцев обучения с ежедневной менторской поддержкой в формате видео 1 на 1 и коммерческими проектами в портфолио

Перейти на курс
Back-end

Карьерный бустер

Получите коммерческий опыт на реальных стартапах, прокачайте tech & soft навыки, научитесь работать в команде, проходить собеседования и получите первую работу в IT!

Перейти на курс
Карьерный бустер

Основы Front-end

Сделайте первый шаг в IT, освоив базовые знания разработки и научившись создавать небольшие проекты на JavaScript

Перейти на курс
Основы Front-end

Основы Back-end

Сделайте первый шаг в IT, освоив базовые знания разработки. Без опыта. Без математики. Только практика: JavaScript, SQL, Node JS, база данных

Перейти на курс
Основы Back-end

useEffect useState fetch

Автор конспекта: Stanislav

📚 Введение

На предыдущих уроках мы разобрали базовый хук useState и основные принципы работы с ним, но на этом занятии разберем не мало важный хук как useEffect который используется в 90% задач при создании веб-приложения с помощью React.

Вот на чем мы остановились:

App.tsx
import { useState } from "react"
 
const tracks = [
  {
    id: 1,
    title: "Musicfun soundtrack",
    url: "https://musicfun.it-incubator.app/api/samurai-way-soundtrack.mp3",
  },
  {
    id: 2,
    title: "Musicfun soundtrack instrumental",
    url: " https://musicfun.it-incubator.app/api/samurai-way-soundtrack-instrumental.mp3",
  },
]
 
export function App() {
  const [selectedTrackId, setSelectedTrackId] = useState(null)
 
  if (tracks === null) {
    return (
      <div>
        <h1>Musicfun</h1>
        <span>loading...</span>
      </div>
    )
  }
 
  if (tracks.length === 0) {
    return (
      <div>
        <h1>Musicfun</h1>
        <span>No tracks</span>
      </div>
    )
  }
 
  return (
    <div>
      <h1>Musicfun</h1>
      <button
        onClick={() => {
          setSelectedTrackId(null)
        }}
      >
        reset selection
      </button>
      <ul>
        {tracks.map((track) => {
          return (
            <li
              key={track.id}
              style={{
                border: track.id === selectedTrackId ? "1px solid orange" : "none",
              }}
            >
              <div
                onClick={() => {
                  setSelectedTrackId(track.id)
                }}
              >
                {track.title}
              </div>
              <audio src={track.url} controls></audio>
            </li>
          )
        })}
      </ul>
    </div>
  )
}

🪄 1. Монтирование и обновление компонент

При первой загрузке приложения компонент проходит стадию монтирования:

  • React инициализирует состояние (например, через useState),
  • запоминает его начальное значение.

Далее, когда мы изменяем это состояние с помощью функций вроде setSelectedTrackId, запускается стадия обновления:

  • React сравнивает текущее дерево элементов с предыдущим (процесс diffing),
  • и в зависимости от различий выборочно перерисовывает компонент или его часть.

Mount (монтирование) — стадия, когда компонент создаётся впервые.

Update (обновление) — стадия, когда React повторно запускает компонент, потому что изменилось его состояние или пришли новые пропсы.

💡

Рендер в контексте React — это не пиксели, которые видит пользователь, а вызов компонента и JSX-разметка, которую этот компонент возвращает.

️️🏗️ 2. useState для tracks

Создадим новый useState где определим две переменных tracks и setTracks, также определим инициализационное состояние этого useState

App.tsx
import { useState } from "react"
 
export function App() {
  const [selectedTrackId, setSelectedTrackId] = useState(null)
  const [tracks, setTracks] = useState([
    {
      id: 1,
      title: "Musicfun soundtrack",
      url: "https://musicfun.it-incubator.app/api/samurai-way-soundtrack.mp3",
    },
    {
      id: 2,
      title: "Musicfun soundtrack instrumental",
      url: " https://musicfun.it-incubator.app/api/samurai-way-soundtrack-instrumental.mp3",
    },
  ])
 
  //...
}

После того как мы добавили и стали использовать переменную tracks из useState, мы увидим, что ничего не изменилось. Это происходит потому, что для инициализации использовалось предыдущее значение переменной, и при монтировании всё осталось прежним.

les11-1

Но если мы определим null для useState как инициализационное значение то мы увидим другой результат

App.tsx
import { useState } from "react"
 
export function App() {
  const [selectedTrackId, setSelectedTrackId] = useState(null)
  const [tracks, setTracks] = useState(null)
 
  //...
}

les11-2

И если мы определим пустой массив для useState как инициализационное значение то мы увидим другой результат

App.tsx
import { useState } from "react"
 
export function App() {
  const [selectedTrackId, setSelectedTrackId] = useState(null)
  const [tracks, setTracks] = useState([])
 
  //...
}

les11-3

И так что мы можем резюмировать что React:

  1. вызывает компонент App
  2. создает для нее Fiber Node регистрируя туда все состояния к примеру наши useState которые мы определили
  3. получает JSX и рисует его пользователю в зависимостях которые мы прописали в компоненте

👨🏻‍🏫 3. effect и useEffect

useEffect — это хук, который позволяет выполнять побочные эффекты (side effects), такие как запросы на сервер или другие действия, на этапе монтирования компонента или при обновлении его состояния/пропсов.

И так посмотрим когда нам нужен useEffect и для этого создадим некоторую функцию effect при вызове которой на этапе монтирования мы сделаем "запрос на сервер".

App.tsx
import { useState } from "react"
 
export function App() {
  const [selectedTrackId, setSelectedTrackId] = useState(null)
  const [tracks, setTracks] = useState([
    {
      id: 1,
      title: "Musicfun soundtrack",
      url: "https://musicfun.it-incubator.app/api/samurai-way-soundtrack.mp3",
    },
    {
      id: 2,
      title: "Musicfun soundtrack instrumental",
      url: " https://musicfun.it-incubator.app/api/samurai-way-soundtrack-instrumental.mp3",
    },
  ])
 
  const effect = () => {
    console.log("effect")
  }
 
  effect()
 
  //...
}

Что же мы увидим:

  1. При монтировании компонента вызывается функция effect.
  2. При изменении состояния через useState — например, при клике на заголовок трека, когда мы изменяем selectedTrackId — происходит обновление компонента и снова вызывается функция effect.

Добавим setTracks в effect и используем те же треки

App.tsx
import { useState } from "react"
 
export function App() {
  const [selectedTrackId, setSelectedTrackId] = useState(null)
  const [tracks, setTracks] = useState([
    {
      id: 1,
      title: "Musicfun soundtrack",
      url: "https://musicfun.it-incubator.app/api/samurai-way-soundtrack.mp3",
    },
    {
      id: 2,
      title: "Musicfun soundtrack instrumental",
      url: " https://musicfun.it-incubator.app/api/samurai-way-soundtrack-instrumental.mp3",
    },
  ])
 
  const effect = () => {
    console.log("effect")
    setTracks([
      {
        id: 1,
        title: "Musicfun soundtrack",
        url: "https://musicfun.it-incubator.app/api/samurai-way-soundtrack.mp3",
      },
      {
        id: 2,
        title: "Musicfun soundtrack instrumental",
        url: " https://musicfun.it-incubator.app/api/samurai-way-soundtrack-instrumental.mp3",
      },
    ])
  }
 
  effect()
 
  //...
}

Что же мы увидим:

  1. При монтировании компонента функция effect вызывается многократно, так как происходит повторный ререндер из-за изменения состояния tracks внутри effect.
  2. Появляется ошибка, сигнализирующая о бесконечном цикле вызовов.

Процесс выглядит так:

  • Монтируется компонент App.
  • Происходит инициализация состояний tracks и selectedTrackId.
  • Вызывается функция effect, которая изменяет состояние tracks с помощью setTracks.
  • Поскольку состояние обновилось, происходит повторный ререндер App, и цикл повторяется.

🎓 3.1 Как работает useEffect

Чтобы избежать лишних рендеров компонента, на помощь приходит хук useEffect. Он позволяет выполнять нужный эффект при монтировании или при изменении определённых условий, которые мы передаем в массив зависимостей — второй аргумент хука.

tsx
useEffect(
  () => {
    //...
  }, // функция - которая выполняется
  [], // массив зависимостей
)
  • Первый аргумент useEffect — это функция с эффектом.
  • Второй аргумент — массив зависимостей.

Если массив зависимостей не передан, эффект выполняется после каждого рендера компонента.

Если массив пустой, эффект выполняется только один раз при монтировании.

🦾 3.2 Используем useEffect

Чтобы установить новое значение tracks только один раз при монтировании и избежать постоянных ререндеров, мы будем использовать хук useEffect установив пустой массив зависимостей.

App.tsx
import { useState, useEffect } from "react"
 
export function App() {
  const [selectedTrackId, setSelectedTrackId] = useState(null)
  const [tracks, setTracks] = useState([
    {
      id: 1,
      title: "Musicfun soundtrack",
      url: "https://musicfun.it-incubator.app/api/samurai-way-soundtrack.mp3",
    },
    {
      id: 2,
      title: "Musicfun soundtrack instrumental",
      url: " https://musicfun.it-incubator.app/api/samurai-way-soundtrack-instrumental.mp3",
    },
  ])
 
  useEffect(() => {
    console.log("effect")
    setTracks([
      {
        id: 1,
        title: "Musicfun soundtrack",
        url: "https://musicfun.it-incubator.app/api/samurai-way-soundtrack.mp3",
      },
      {
        id: 2,
        title: "Musicfun soundtrack instrumental",
        url: " https://musicfun.it-incubator.app/api/samurai-way-soundtrack-instrumental.mp3",
      },
    ])
  }, [])
 
  //...
}

📡 4. Выполняем запрос на сервер

Итак, давайте сделаем запрос к серверу. Для этого нам понадобится:

  1. fetch — с помощью этого метода мы выполним запрос.
  2. URL-адрес — это первый аргумент, который мы передаём в вызов fetch:
url
"https://musicfun.it-incubator.app/api/1.0/playlists/tracks"
  1. API-ключ — параметр, который мы передаём в заголовок запроса. Этот ключ нужно взять на нашем сайте apihub
App.tsx
import { useState, useEffect } from "react"
 
export function App() {
  const [selectedTrackId, setSelectedTrackId] = useState(null)
  const [tracks, setTracks] = useState([
    {
      id: 1,
      title: "Musicfun soundtrack",
      url: "https://musicfun.it-incubator.app/api/samurai-way-soundtrack.mp3",
    },
    {
      id: 2,
      title: "Musicfun soundtrack instrumental",
      url: " https://musicfun.it-incubator.app/api/samurai-way-soundtrack-instrumental.mp3",
    },
  ])
 
  useEffect(() => {
    console.log("effect")
 
    fetch("https://musicfun.it-incubator.app/api/1.0/playlists/tracks", {
      headers: {
        "api-key": "0e0d1907-125b-437f-bba3-488ac8d73c4b",
      },
    })
      .then((res) => res.json())
      .then((json) => {
        setTracks(json)
      })
  }, [])
 
  //...
}

Что происходит:

  1. При обновлении страницы первое, что мы увидим, — это данные, которые были заданы в стейте до выполнения запроса:

les11-4

  1. После выполнения запроса всё пропадает. Почему это происходит и что случилось? Нам нужно исследовать эту проблему.

🧐 5. Анализ и решение ошибки

После выполнения запроса мы получаем некоторые данные, которые необходимо отобразить. Важно понять, какова структура этих данных и как правильно использовать полученный ответ.

Как мы можем проанализировать, что именно пошло не так и почему сайт работает не так, как ожидается:

  1. Откройте вкладку Sources в панели разработчика, попасть в которую можно с помощью сочетания клавиш fn + F12 или просто F12.
  2. Найдите файл App.tsx.

les11-5

  1. Поставьте точку останова на строке, где используется метод map для предполагаемого массива, затем перезагрузите страницу с помощью комбинации клавиш command + R или ctrl + R.

les11-6

  1. При наведении курсора вы обнаружите, что это не массив, а объект, который содержит совершенно иную структуру данных. Эту структуру необходимо проанализировать и корректно использовать.

Итак, предполагается, что в ответе сервера есть поле data, в котором содержится массив треков для отображения. Поэтому при сохранении данных в состояние необходимо сетать не просто весь объект json, а именно json.data.

App.tsx
import { useState, useEffect } from "react"
 
export function App() {
  const [selectedTrackId, setSelectedTrackId] = useState(null)
  const [tracks, setTracks] = useState(null)
 
  useEffect(() => {
    console.log("effect")
 
    fetch("https://musicfun.it-incubator.app/api/1.0/playlists/tracks", {
      headers: {
        "api-key": "0e0d1907-125b-437f-bba3-488ac8d73c4b",
      },
    })
      .then((res) => res.json())
      .then((json) => {
        setTracks(json.data)
      })
  }, [])
 
  //...
}

Посмотрим, что получилось:

les11-7

Удалось отобразить треки, но не отображаются их названия, и отсутствует возможность воспроизведения треков. Разберёмся, в чём причина:

  1. Снова открываем панель разработчика, переходим во вкладку Sources и смотрим структуру данных.

les11-8

Как видно, поле title находится внутри вложенного объекта attributes, то есть получить доступ к названию трека можно по ключу track.attributes.title.

App.tsx
//...
return (
  <div>
    <h1>Musicfun</h1>
    <button
      onClick={() => {
        setSelectedTrackId(null)
      }}
    >
      reset selection
    </button>
    <ul>
      {tracks.map((track) => {
        return (
          <li
            key={track.id}
            style={{
              border: track.id === selectedTrackId ? "1px solid orange" : "none",
            }}
          >
            <div
              onClick={() => {
                setSelectedTrackId(track.id)
              }}
            >
              {track.attributes.title}
            </div>
            <audio src={track.url} controls></audio>
          </li>
        )
      })}
    </ul>
  </div>
)
  1. Одну проблему мы решили — теперь при обновлении страницы явно видно, что название трека отображается корректно:

les11-9

Однако треки по-прежнему не воспроизводятся. Проверим путь к url.

les11-10

Видим, что нужный url находится внутри объекта attributes, далее — по ключу attachments, который является массивом. Внутри первого элемента этого массива находится свойство url.

App.tsx
//... return (
 
<div>
  <h1>Musicfun</h1>
  <button
    onClick={() => {
      setSelectedTrackId(null)
    }}
  >
    reset selection
  </button>
  <ul>
    {tracks.map((track) => {
      return (
        <li
          key={track.id}
          style={{
            border: track.id === selectedTrackId ? "1px solid orange" : "none",
          }}
        >
          <div
            onClick={() => {
              setSelectedTrackId(track.id)
            }}
          >
            {track.attributes.title}
          </div>
          <audio src={track.attributes.attachments[0].url} controls></audio>
        </li>
      )
    })}
  </ul>
</div>
)

Теперь треки можно воспроизводить, и все возникшие проблемы были исправлены:

les11-11

⚙️ 6. Lifecycle Mount Mode

les11-12

Разберём жизненный цикл компоненты App при монтировании:

  1. React создаёт для компоненты объект FiberNode, который будет хранить "состояния" этой компоненты. То есть React готовит место для хранения всех необходимых данных компоненты.
  2. Затем React вызывает функцию-компоненту, начинается render phase.
  3. Происходит инициализация начальных состояний, например, которые мы определили с помощью useState.
  4. Фиксируются действия, которые нужно будет выполнить позже, например, какая функция из useEffect должна быть вызвана.
  5. Компонента возвращает JSX — объект ReactElement, который может содержать другие объекты. Этот объект возвращается в React.
  6. Затем React переходит к следующей фазе — commit phase, когда на основе полученной информации создаётся DOM.
  7. Можно увидеть визуальный результат на странице.
  8. После визуализации React приступает к выполнению зарегистрированных ранее в очередь функций. В нашем примере был зарегистрирован useEffect, который теперь выполняется.
  9. Внутри useEffect мы делаем запрос к серверу через fetch и получаем ответ.
  10. После получения ответа мы обновляем состояние с помощью setTracks(serverTracks), фиксируя новое значение в FiberNode.

🔃 7. Lifecycle Update Mode

les11-13

Итак, мы зафиксировали новое значение, и tracks изменились. React видит, что данные, влияющие на отрисовку, изменились, поэтому нужно создать новый результат на основе новых данных, и React переходит к этапу обновления:

  1. React повторно вызывает компоненту App, чтобы обновить DOM, который видит пользователь. На этот раз useEffect не будет вызван, так как он срабатывает только один раз при монтировании.
  2. После этого React снова переходит в render phase, где формируется новый обновлённый ReactElement.
  3. Затем начинается commit phase, и React обновляет DOM на основе новой информации.
  4. Теперь можно увидеть визуальный результат после обновления.

🏠 Домашнее задание

Цель задания:

Практически освоить использование хука useEffect для загрузки данных с сервера и понять жизненный цикл React компонента.

Задание 1

Получение данные с сервера

Продолжай работать в проекте с предыдущего домашнего задания.

  1. В App.tsx создай состояние tasks с помощью useState, установив изначальное значение в null
  2. Удали хардкодный массив tasks из глобальной области видимости
  3. Напиши useEffect с пустым массивом зависимостей []
  4. В эффекте выполни fetch-запрос к API за получением тасок
  5. Используй API endpoint: https://trelly.it-incubator.app/api/1.0/boards/tasks

Структура ответа с сервера должна выглядеть вот так 🚀

hw-11-server-response

Количество тасок, их заголовки и другие свойства могут отличаться, т.к. это список всех публичных тасок

Задание 2

Отрисовка данных с сервера

После того как запрос отрабатывает верно перейдем к отрисовке данных

  1. Преобразуй ответ в JSON
  2. Установи полученные данные в состояние с помощью setTasks
  3. Структура данных пришедшая с сервера будет немного отличаться от данных, которые мы хранили в переменной tasks. Поэтому внимательно изучи ответ с сервера и отрисуй данные в соответствии со структурой которая пришла с сервера

❗ Статус

Для отображения статуса таски мы использовали свойство isDone. Но сервер для работы со статусом возвращает свойство status, которое может быть в 4-х состояниях

text
- 0 - Таска не выполнена
- 1 - Таска в процесс выполнения
- 2 - Таска выполнена
- 3 - Черновик

Соответственно при отрисовке данных замени свойство isDone на status. status === 0 обозначает невыполненную таску, а status === 2 - выполненную.

Итоговый результат 🚀

hw-11-server-response-ui

Задание 3

Преобразуй дату в понятный для человека формат.

Для реализации данной задача необходимо воспользоваться объектом Date и вызвать у него метод .toLocaleDateString()

🔗

.toLocaleDateString() — преобразует дату в строку, отформатированную в соответствии с локальными настройками пользователя (например: 17.04.2025 для русской локали или 4/17/2025 для США)

tsx
<p>
  <b>Дата создания задачи</b>: {new Date(task.attributes.addedAt).toLocaleDateString()}
</p>

hw-11-date

Доп. задания

Отладка (Debug)

  1. Открой DevTools (F12)
  2. Во вкладке Network проверь, что запрос отправляется только один раз при загрузке компонента
  3. При клике на задачи убедись, что дополнительные запросы не отправляются

Ожидаемый результат

  • При загрузке страницы должен показываться индикатор "Загрузка..."
  • После получения данных с сервера должен отобразиться список задач
  • Функциональность выделения задач должна работать как прежде
  • Запрос должен выполняться только один раз при монтировании компонента

Вопросы для самопроверки

  1. Почему мы используем пустой массив зависимостей [] в useEffect?
  2. Что произойдет, если не передать массив зависимостей вообще?
  3. В какой момент жизненного цикла компонента выполняется эффект с пустым массивом зависимостей?
  4. Почему нельзя вызывать хуки внутри условий или циклов?

💡

Удачи в изучении React! Помни: понимание useEffect — это ключ к освоению современной разработки на React.

Боевой маршрут (React Путь Самурая: без альтернатив)

Видеоурок - 13 видео из 30