Работа с API в React

Курс посвящён практической интеграции внешних API в React-приложения. Рассмотрим способы получения данных, обработку ошибок, управление состоянием запросов и базовые подходы к архитектуре и безопасности клиентских интеграций.

1. Основы API и HTTP для React-разработчика

Основы API и HTTP для React-разработчика

Зачем React-разработчику понимать API и HTTP

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

  • что такое API и какие бывают API
  • как устроен HTTP-запрос и HTTP-ответ
  • какие есть методы, коды статуса, заголовки
  • как передаются данные (JSON, форма, файлы)
  • какие типовые проблемы возникают в браузере (например, CORS)
  • В следующих статьях курса мы будем использовать эти основы, чтобы правильно проектировать запросы, обрабатывать ошибки, управлять состоянием загрузки и выстраивать надёжную работу с сервером в React.

    Что такое API

    API (Application Programming Interface) — это способ, с помощью которого одна программа предоставляет другой программе доступ к своим функциям или данным.

    В контексте веба чаще всего под API понимают HTTP API: набор URL-адресов (эндпоинтов), по которым можно отправлять HTTP-запросы и получать ответы.

    Примеры задач, которые решает API:

  • получить список товаров
  • зарегистрировать пользователя
  • обновить профиль
  • загрузить изображение
  • Полезный справочник по HTTP на русском/английском: MDN Web Docs — HTTP

    Клиент и сервер: как это выглядит в React

    React-приложение в браузере — это клиент. Оно отправляет запросы к серверу, который:

  • проверяет права
  • валидирует данные
  • читает/пишет в базу
  • формирует ответ
  • !Схема обмена HTTP-запросом и ответом между React-приложением и API

    HTTP простыми словами

    HTTP — это протокол, по которому клиент и сервер обмениваются сообщениями.

    Каждый запрос обычно состоит из:

  • метода (что сделать)
  • URL (где сделать)
  • заголовков (служебные метаданные)
  • тела (данные, если нужно)
  • Каждый ответ обычно содержит:

  • статус-код (результат выполнения)
  • заголовки
  • тело ответа (данные или описание ошибки)
  • Официальный обзор: MDN — HTTP overview

    URL и эндпоинты

    URL часто включает несколько смысловых частей:

  • origin: протокол + домен + порт (например, https://api.example.com)
  • path: путь к ресурсу (например, /users/42)
  • query string: параметры после ? (например, ?page=2&sort=name)
  • Пример:

  • https://api.example.com/users/42?details=full
  • Здесь:

  • ресурс: пользователь с идентификатором 42
  • query-параметр details=full уточняет формат ответа
  • !Разбор частей URL на примере

    Методы HTTP (что мы просим сделать)

    На практике в API чаще всего встречаются:

  • GET — получить данные (обычно без изменения на сервере)
  • POST — создать ресурс или выполнить действие
  • PUT — заменить ресурс целиком
  • PATCH — частично обновить ресурс
  • DELETE — удалить ресурс
  • Важно: у методов есть ожидаемая семантика. Например, GET в корректном API не должен изменять данные, потому что браузеры и прокси могут кэшировать GET-запросы.

    Справочник: MDN — HTTP request methods

    Коды статуса HTTP (как сервер сообщает результат)

    Статус-код — это число, которое помогает быстро понять, что произошло.

    Успешные ответы

  • 200 OK — запрос выполнен, ответ содержит данные
  • 201 Created — ресурс создан (часто возвращают созданный объект)
  • 204 No Content — успешно, но тело ответа пустое (например, после удаления)
  • Ошибки клиента

  • 400 Bad Request — некорректные данные (например, не прошла валидация)
  • 401 Unauthorized — нужна аутентификация (не переданы или неверны учётные данные)
  • 403 Forbidden — доступ запрещён (вы аутентифицированы, но прав нет)
  • 404 Not Found — ресурс не найден
  • 409 Conflict — конфликт (например, попытка создать пользователя с уже занятым email)
  • 429 Too Many Requests — слишком много запросов (лимиты)
  • Ошибки сервера

  • 500 Internal Server Error — внутренняя ошибка
  • 503 Service Unavailable — сервис временно недоступен
  • Справочник: MDN — HTTP response status codes

    Заголовки HTTP (headers)

    Заголовки — это метаданные про запрос или ответ.

    Часто используемые:

  • Content-Type — формат тела (например, application/json)
  • Accept — какие форматы ответа клиент готов принять
  • Authorization — данные для авторизации (часто токен)
  • Cache-Control — правила кэширования
  • Пример: если вы отправляете JSON на сервер, обычно указывают:

  • Content-Type: application/json
  • Справочник: MDN — HTTP headers

    Тело запроса и форматы данных

    JSON

    Самый частый формат обмена данными в API.

  • удобно читать и отлаживать
  • естественно ложится на JavaScript-объекты
  • Пример тела запроса (JSON):

    FormData (формы и файлы)

    Если нужно отправить файл (например, аватар), часто используют FormData.

  • браузер сам выставляет правильный Content-Type с multipart/form-data и boundary
  • удобно добавлять файлы и поля
  • URL-encoded

    Иногда встречается формат application/x-www-form-urlencoded (например, в старых системах или OAuth-сценариях).

    REST-подход на базовом уровне

    REST — это распространённый стиль проектирования API, где ресурсы адресуются URL-ами.

    Типичные примеры:

  • GET /products — список товаров
  • GET /products/10 — товар с id 10
  • POST /products — создать товар
  • PATCH /products/10 — обновить часть полей
  • DELETE /products/10 — удалить
  • Важно: это стиль, а не жёсткий стандарт. Реальные API могут отступать от REST.

    Идемпотентность: почему это важно

    Идемпотентность означает: повторение запроса даёт тот же эффект, что и один вызов.

    Практический смысл для фронтенда:

  • при сетевых сбоях запрос может быть отправлен повторно
  • при повторе GET обычно безопасен
  • PUT и DELETE часто проектируют идемпотентными
  • POST обычно не идемпотентен (повтор может создать дубль)
  • Это влияет на то, как вы строите логику повторных запросов в React (например, при авто-ретраях).

    Ошибки и их формат

    В идеале сервер возвращает ошибки в структурированном виде (часто JSON). Например:

    Тогда в React можно:

  • показать общее сообщение пользователю
  • подсветить конкретные поля
  • логировать code для диагностики
  • Если API не возвращает структурированные ошибки, обычно всё равно есть:

  • статус-код
  • строка сообщения
  • Браузерные ограничения: CORS

    Если React-приложение загружено, например, с http://localhost:5173, а API находится на https://api.example.com, это разные origin. Браузер применяет политику безопасности, и запросы между origin контролируются механизмом CORS.

    Ключевые моменты:

  • CORS настраивается на стороне сервера (не «чинится» только фронтендом)
  • сервер должен вернуть корректные заголовки, например Access-Control-Allow-Origin
  • некоторые запросы вызывают preflight-запрос OPTIONS
  • Справочник: MDN — CORS

    Аутентификация и авторизация: база терминов

    Важно различать:

  • аутентификация — кто вы (проверка личности)
  • авторизация — что вам можно (проверка прав)
  • Популярные способы в веб-приложениях:

  • cookie-сессии (часто с HttpOnly cookie)
  • токены (часто через заголовок Authorization: Bearer <token>)
  • React-клиент обычно:

  • отправляет токен/куки вместе с запросом
  • реагирует на 401 (например, разлогинить или обновить токен)
  • HTTPS

    Для реальных приложений важно использовать https://:

  • шифрование трафика
  • защита от подмены содержимого
  • защита токенов и пользовательских данных при передаче
  • Справочник: MDN — HTTPS

    Минимальный пример запроса из браузера

    Хотя в следующих статьях мы будем разбирать запросы в React более системно, полезно увидеть базовый пример с fetch.

    GET запрос:

    Документация: MDN — fetch()

    Частые практические выводы для React

  • Всегда различайте: ошибка сети (запрос не дошёл) и ошибка API (дошёл, но вернулся 4xx/5xx).
  • Смотрите на status и на формат ответа, не полагайтесь только на текст сообщения.
  • Договоритесь в команде о едином формате ошибок и единых правилах: какие статусы для каких ситуаций.
  • Продумайте, какие запросы можно безопасно повторять.
  • Учитывайте CORS уже на этапе подключения API к фронтенду.
  • Что дальше по курсу

    Следующий шаг — научиться организовывать работу с запросами в React так, чтобы:

  • показывать состояния loading / success / error
  • отменять устаревшие запросы
  • избегать гонок данных при быстром переключении экранов
  • выстраивать слой API-клиента и переиспользуемые функции
  • Эти темы мы будем разворачивать дальше, опираясь на HTTP-основы из этой статьи.

    2. Fetch и Axios: выполнение запросов в React

    Fetch и Axios: выполнение запросов в React

    Как эта тема связана с HTTP-основами

    В предыдущей статье мы разобрали, из чего состоит HTTP-запрос и ответ: методы, заголовки, коды статуса, тело (например, JSON), а также частые проблемы вроде CORS. Теперь применим эти знания на практике в React и посмотрим на два основных инструмента для выполнения запросов из браузера:

  • Fetch API — встроен в браузер.
  • Axios — популярная библиотека поверх XHR/Fetch с удобными возможностями.
  • Наша цель в этой статье: научиться уверенно делать запросы, обрабатывать ошибки и понимать, что именно происходит на уровне HTTP.

    !Схема различий в типичном пайплайне обработки ответа в Fetch и Axios

    Fetch API в React

    Базовый GET-запрос

    fetch возвращает Promise, который резолвится, когда пришёл HTTP-ответ. Важно: сам факт ответа не означает успех по бизнес-логике или по статус-коду.

    Важное отличие Fetch: catch не ловит HTTP 404/500

    У fetch есть два разных класса проблем:

  • ошибка сети: нет соединения, DNS, CORS блокировка, запрос прерван
  • ошибка API: сервер ответил статусом 4xx/5xx
  • fetch обычно попадает в catch только при ошибках сети или при прерывании запроса, а при 404 вернётся обычный response, где response.ok === false.

    Axios в React

    Установка и базовый GET

    У Axios удобный интерфейс и полезные возможности из коробки.

    Документация: Axios — Introduction

    Пример запроса:

    POST с JSON

    Важное отличие Axios: HTTP 4xx/5xx обычно попадают в catch

    Axios по умолчанию считает ответы со статусами вне диапазона 200–299 ошибками промиса. Это часто упрощает обработку.

    Пример:

    Fetch vs Axios: практическое сравнение

    | Критерий | Fetch | Axios | |---|---|---| | Доступность | Встроен в браузер | Нужна установка пакета | | JSON | Нужно вручную: await response.json() | Обычно уже в response.data | | HTTP-ошибки 4xx/5xx | Не catch, нужно if (!response.ok) | Обычно catch по умолчанию | | Таймауты | Нужна своя логика | Есть параметр timeout | | Интерцепторы | Нет встроенных | Есть interceptors | | Отмена запроса | AbortController | Поддерживает AbortController в современных версиях |

    Выбор инструмента чаще зависит от того, нужен ли вам:

  • минимализм и отсутствие зависимостей
  • удобная инфраструктура вокруг запросов (таймауты, интерцепторы, единый клиент)
  • Запросы в React: где именно вызывать fetch или axios

    Самая частая точка: useEffect

    В React запросы на загрузку данных обычно делают при монтировании компонента или при изменении зависимостей.

    Справочник: React — useEffect

    Пример с fetch:

    Здесь ignore предотвращает обновление состояния, если компонент уже размонтирован или эффект стал неактуальным.

    Отмена запроса: AbortController

    Когда пользователь быстро переключает страницы, старые запросы могут стать неактуальными. Тогда полезно прерывать запрос.

    Документация: MDN — AbortController

    Пример с fetch + отмена:

    Пример обработки 401:

    Важно:

  • интерцепторы должны быть предсказуемыми, иначе диагностика ошибок усложняется
  • не стоит бесконтрольно делать ретраи внутри интерцепторов без защиты от циклов
  • Cookies, CORS и credentials

    Если ваш API использует cookie-сессии (например, HttpOnly cookie), то в браузере часто нужно явно разрешить отправку cookies.

    Для fetch:

    Для Axios:

    Если CORS настроен неправильно, браузер заблокирует запрос. Это возвращает нас к предыдущей статье: CORS решается на сервере, а фронтенд лишь выбирает корректные флаги (credentials, withCredentials) согласно требованиям API.

    Типовые ошибки и как их избежать

  • Смешивать сетевые и HTTP-ошибки
  • Забывать про response.ok в Fetch
  • Пытаться читать JSON, когда тело пустое
  • Обновлять state после unmount
  • Делать запросы прямо в рендере компонента
  • Что дальше

    Теперь у вас есть два рабочих инструмента для запросов и понимание их различий. Следующий логичный шаг в курсе — организовать работу с данными поверх запросов так, чтобы:

  • единообразно вести состояния loading / success / error
  • отменять и дедуплицировать запросы
  • избегать гонок данных при быстрых изменениях UI
  • аккуратно работать с авторизацией и обновлением токенов
  • 3. useEffect и жизненный цикл запросов

    useEffect и жизненный цикл запросов

    Зачем React-разработчику понимать жизненный цикл запросов

    В предыдущих статьях мы разобрали основы HTTP и научились выполнять запросы через fetch и Axios. Следующий шаг в реальном React-приложении — правильно встроить запросы в жизненный цикл UI так, чтобы:

  • запросы запускались в нужный момент
  • при смене параметров (например, query, userId) данные обновлялись предсказуемо
  • не происходило обновление state после размонтирования
  • не появлялись гонки данных, когда старый ответ перезаписывает новый
  • корректно обрабатывались ошибки сети и ошибки API
  • В React за это чаще всего отвечает связка useEffect + cleanup-функция.

    !Диаграмма показывает, когда запускается эффект, когда нужно делать cleanup, и как отменять устаревшие запросы

    Что такое useEffect и почему запросы обычно делают там

    useEffect — это хук для выполнения побочных эффектов после того, как React отрисовал компонент.

    Побочный эффект — это действие, которое выходит за пределы вычисления JSX: сетевой запрос, подписка, таймер, работа с DOM-API браузера.

    Ключевая причина, почему запрос нельзя делать прямо в рендере:

  • рендер должен оставаться чистым и предсказуемым
  • React может вызывать рендер чаще, чем вы ожидаете
  • в React 18 в режиме разработки со StrictMode эффекты могут выполняться дополнительно для проверки корректности
  • Официальные материалы:

  • useEffect
  • Synchronizing with Effects
  • Модель жизненного цикла: mount, update, unmount

    Хотя в функциональных компонентах нет методов классового жизненного цикла, мыслить удобно так:

  • mount — компонент появился на странице
  • update — компонент перерисовался из-за изменения props или state
  • unmount — компонент исчез со страницы
  • useEffect помогает привязать сетевой запрос к этим этапам.

    Как работает useEffect: зависимости и cleanup

    Сигнатура:

    Важные правила:

  • если массив зависимостей [], эффект запускается после первого появления компонента и больше не перезапускается (до размонтирования)
  • если зависимости указаны, эффект перезапускается при изменении любой зависимости
  • cleanup-функция запускается:
  • - перед повторным запуском эффекта (когда зависимости изменились) - при размонтировании компонента

    Именно cleanup делает жизненный цикл запросов управляемым.

    Базовый шаблон запроса: loading / error / data

    Ниже шаблон, который полезно держать в голове. Он работает и с fetch, и с Axios, но пример покажем на fetch.

    Почему здесь есть cancelled:

  • запрос может завершиться после того, как компонент уже размонтирован
  • без защиты вы рискуете вызвать setState для компонента, который больше не актуален
  • Это не отменяет запрос физически, но предотвращает побочные последствия.

    Отмена запросов: AbortController как корректный cleanup

    Для fetch стандартный способ отмены — AbortController.

    Документация:

  • AbortController
  • Fetch API
  • Пример: при изменении query мы отменяем прошлый запрос, чтобы:

  • не тратить сеть впустую
  • не допустить гонку данных
  • Axios и отмена запросов через signal

    Если вы используете Axios, в современных версиях можно передавать signal от AbortController аналогично fetch.

    Документация:

  • Axios AbortController
  • Идея такая же:

  • создаём controller
  • передаём signal в запрос
  • в cleanup вызываем abort
  • Минимальный чек-лист для запросов в useEffect

  • храните состояния loading / error / data (или используйте общий слой данных, если он будет позже по курсу)
  • различайте ошибки сети и ответы API со статусом 4xx/5xx
  • предотвращайте гонки: AbortController или идентификатор запроса
  • добавляйте cleanup, чтобы не обновлять состояние для неактуального компонента
  • внимательно относитесь к зависимостям эффекта
  • Что дальше по курсу

    Теперь вы понимаете, когда запускать запросы и как управлять их жизненным циклом в React. Следующие логичные темы для развития слоя работы с API:

  • переиспользуемые хуки для запросов
  • унификация обработки ошибок и статусов
  • авторизация: обновление токенов, повтор запросов, защита от циклов
  • кэширование и дедупликация запросов на уровне приложения
  • 4. Состояния загрузки, ошибки и повторные запросы

    Состояния загрузки, ошибки и повторные запросы

    Зачем нужны состояния и ретраи

    В прошлых статьях курса вы научились:

  • понимать, что означает HTTP-статус и чем 4xx отличается от 5xx
  • делать запросы через fetch и Axios и не путать сетевые ошибки с ошибками API
  • запускать запросы в useEffect, отменять их и избегать гонок данных
  • Теперь соберём это в практичный паттерн для UI: как хранить состояния загрузки, как показывать ошибки пользователю и как делать повторные запросы так, чтобы приложение оставалось предсказуемым.

    !Схема жизненного цикла запроса как конечного автомата

    Ментальная модель: запрос как конечный автомат

    Для большинства запросов в UI достаточно четырёх состояний:

  • idle — запрос ещё не запускали (например, экран открылся, но query пустой)
  • loading — запрос выполняется
  • success — данные успешно получены
  • error — произошла ошибка
  • Важно понимать, что состояние запроса — это не только переменная loading. В реальном UI часто нужен минимальный набор:

  • data — данные (или null)
  • error — объект ошибки (или null)
  • status или loading — индикатор процесса
  • Базовый шаблон состояния: data / loading / error

    Этот шаблон удобен тем, что:

  • легко отрисовать UI для каждого состояния
  • легко сбрасывать ошибку перед новым запуском
  • легко повторять запрос
  • Пример на fetch (вынесем загрузку в функцию и добавим кнопку Повторить):

    Пример использования в компоненте:

    Что это даёт:

  • повторяем только то, что действительно может “само починиться”
  • сохраняем поддержку отмены
  • не раздуваем логику ретрая внутри каждого компонента
  • Axios: как это отражается на обработке ошибок

    Если вы используете Axios, то отличие в том, что 4xx/5xx обычно приходят в catch, и там часто доступно:

  • error.response.status
  • error.response.data
  • Поэтому паттерн “распознаём статус и решаем, ретраить или нет” обычно делают в одном месте: либо в функции запроса, либо в интерцепторе.

    Ссылка:

  • Документация Axios: Handling Errors
  • Дедупликация и защита от гонок при ретраях

    Когда вы добавляете ретраи, особенно важно не допустить ситуацию:

  • компонент уже получил новый параметр (например, новый query)
  • старый запрос ещё ретраится
  • старый запрос наконец “успешен” и перезаписывает актуальные данные
  • Практические способы защиты:

  • отменять устаревший запрос через AbortController
  • использовать “идентификатор запроса” (как в статье про гонки данных), и обновлять state только для самого свежего запроса
  • В большинстве React-приложений проще всего сочетать:

  • AbortController для отмены
  • аккуратные зависимости useEffect
  • Практический чек-лист

  • Выделяйте состояния loading / error / data (или status).
  • В fetch проверяйте response.ok и превращайте 4xx/5xx в ошибки.
  • Сбрасывайте error перед новой попыткой.
  • Делайте повторные запросы осознанно: обычно только для GET и временных ошибок.
  • При изменении параметров отменяйте старые запросы, чтобы не ловить гонки.
  • Что дальше по курсу

    После того как вы научились управлять состояниями и ретраями, следующий логичный шаг — вынести повторяющуюся логику в переиспользуемые решения:

  • общий API-клиент
  • переиспользуемые хуки для запросов
  • единый формат ошибок для UI
  • более продвинутая работа с кэшированием и дедупликацией
  • 5. Отмена запросов, дебаунс и предотвращение гонок

    Отмена запросов, дебаунс и предотвращение гонок

    Зачем это нужно в React-приложении с API

    В предыдущих статьях курса вы уже научились:

  • делать запросы через fetch и Axios
  • запускать запросы в useEffect и делать cleanup
  • держать состояния loading / error / data
  • осознанно повторять запросы (retry)
  • Но в реальном UI почти сразу появляются три практические проблемы:

  • отмена запроса: пользователь ушёл со страницы или поменял параметры, а старый запрос ещё выполняется
  • дебаунс: пользователь быстро вводит текст, а приложение не должно отправлять запрос на каждый символ
  • гонки данных: ответы приходят не в том порядке, в котором запросы были отправлены, и UI может показать устаревшие данные
  • Эта статья соберёт эти темы в один рабочий набор паттернов.

    !Визуальная демонстрация гонки данных и способов защиты

    Термины простыми словами

  • Отмена запроса: вы прерываете запрос, который больше не нужен, чтобы он не тратил сеть и не пытался обновить state.
  • Дебаунс: вы ждёте паузу в действиях пользователя (например, 300 мс после последнего ввода), и только потом делаете запрос.
  • Гонка данных: несколько запросов “соревнуются”, и более старый ответ может прийти позже и перезаписать актуальные данные.
  • Отмена запросов в React через AbortController

    Почему простого флага cancelled иногда недостаточно

    В статье про useEffect мы использовали паттерн cancelled, чтобы не вызывать setState после размонтирования. Это полезно, но у него есть минусы:

  • запрос всё равно продолжает выполняться
  • сервер всё равно получает запрос
  • в случае больших ответов вы всё равно тратите трафик
  • Поэтому лучший вариант для fetch и для современного Axios — отменять запрос физически.

    Документация:

  • AbortController (MDN)
  • fetch (MDN)
  • Базовый шаблон отмены в useEffect

    Практическая деталь:

  • у Axios имя ошибки отмены может отличаться (часто CanceledError), поэтому удобно проверять документацию и вашу версию Axios
  • Гонки данных: как они возникают и почему отмена не всегда решает всё

    Типичный сценарий гонки

  • пользователь вводит поисковый запрос
  • на каждое изменение query вы запускаете useEffect
  • запрос для старого query может завершиться позже и перезаписать данные
  • Отмена через AbortController обычно решает проблему, но есть ситуации, где полезно иметь дополнительную защиту:

  • не все клиенты/окружения корректно отменяют (например, особые обёртки)
  • часть логики выполняется до запроса или после него, и её тоже важно “погасить”
  • вы делаете несколько запросов параллельно и собираете результат
  • Паттерн requestId: обновляем state только для самого свежего запроса

    Документация:

  • useRef (React)
  • Здесь сочетаются сразу два механизма:

  • дебаунс снижает количество запросов
  • AbortController защищает от гонок, если пользователь успел изменить запрос до завершения предыдущего
  • Вариант с библиотекой

    Иногда используют готовую реализацию дебаунса, например lodash.debounce.

    Документация:

  • debounce (Lodash)
  • Но в React важно помнить: если вы создаёте debounced-функцию на каждый рендер, дебаунс “сломается”. Обычно тогда применяют useMemo или useCallback для стабильной ссылки.

    Комбинации паттернов: что выбирать на практике

    Быстрый выбор

    | Ситуация | Рекомендуемый паттерн | |---|---| | Компонент размонтируется, а запрос ещё идёт | AbortController в cleanup | | Параметр запроса часто меняется (поиск) | дебаунс + AbortController | | Есть риск устаревших ответов (гонки) | AbortController и при необходимости requestId | | Нельзя или неудобно отменить запрос | requestId и игнорирование устаревшего ответа |

    Практическое правило для курса

    Если вы делаете запрос внутри useEffect, то почти всегда стоит задать себе три вопроса:

  • Нужно ли отменять запрос при смене параметра или размонтировании?
  • Может ли пользователь менять параметр слишком часто (нужен ли дебаунс)?
  • Что произойдёт, если ответы придут не по порядку (есть ли защита от гонок)?
  • Частые ошибки

  • забыть обработать AbortError и показывать отмену как “ошибку” пользователю
  • дебаунсить функцию запроса, но пересоздавать её на каждом рендере
  • очищать результаты на каждый новый запрос и получать “мигание” UI вместо плавного обновления
  • полагаться только на loading и не учитывать гонки данных
  • Как это связывается с предыдущими темами курса

  • Из статьи про fetch и Axios: важно помнить, что fetch не считает 4xx/5xx ошибкой промиса, а значит вы сами превращаете статус в throw.
  • Из статьи про useEffect: cleanup — ключевое место, где вы отменяете запрос и делаете жизненный цикл предсказуемым.
  • Из статьи про состояния и ретраи: дебаунс и отмена помогают уменьшить количество запросов и снизить вероятность лишних ретраев, а также сделать loading/error более “чистыми” для пользователя.
  • Если вы дальше будете выносить запросы в переиспользуемые хуки или API-клиент, эти механизмы (abort, дебаунс, защита от гонок) стоит закладывать на уровне общих утилит, а не копировать по компонентам.

    6. Аутентификация: токены, refresh и защищённые запросы

    Аутентификация: токены, refresh и защищённые запросы

    Зачем это нужно в курсе про API в React

    До этого в курсе мы научились:

  • делать запросы через fetch и Axios
  • правильно запускать запросы в useEffect и делать cleanup
  • держать состояния loading / error / data, делать ретраи и отмену
  • избегать гонок с AbortController и requestId
  • Аутентификация добавляет к этому ещё один важный слой: не каждый запрос можно выполнять анонимно. Как только API требует вход, появляются задачи:

  • передавать учётные данные (токен или cookie)
  • защищать запросы (добавлять Authorization, включать cookies)
  • правильно обрабатывать 401 Unauthorized
  • обновлять истёкший токен через refresh и повторять запрос
  • не допускать бесконечных циклов обновления и гонок при параллельных запросах
  • Базовые термины

  • Аутентификация: проверка кто вы (например, вход по логину и паролю).
  • Авторизация: проверка что вам можно (например, доступ к /admin).
  • Access token: короткоживущий токен для запросов к API.
  • Refresh token: более долгоживущий секрет, который позволяет получить новый access token.
  • Bearer token: токен, который передаётся в заголовке Authorization: Bearer <token>.
  • Полезные справочники:

  • MDN: HTTP authentication
  • RFC 6750: Bearer Token Usage
  • Два основных подхода в браузере

    В React-приложениях чаще всего встречаются два сценария.

    Cookie-сессии

    Сервер устанавливает cookie, браузер автоматически отправляет её на нужный домен.

    Плюсы:

  • удобно, не нужно вручную добавлять заголовок Authorization
  • можно хранить сессию в HttpOnly cookie (недоступно JavaScript)
  • Минусы:

  • нужно учитывать CSRF
  • при CORS часто требуются настройки credentials и заголовков на сервере
  • Как включить cookies:

  • для fetch: credentials: "include"
  • для Axios: withCredentials: true
  • Справочник:

  • MDN: Request.credentials
  • Access/Refresh токены

    Обычно:

  • access token живёт недолго (минуты)
  • refresh token живёт дольше (дни/недели)
  • Важно: в реальных системах refresh token часто кладут в HttpOnly cookie, а access token держат в памяти приложения. Это снижает риск кражи токенов при XSS.

    Рекомендации по рискам токенов:

  • OWASP: Token-Based Authentication
  • Где хранить токены в React: практичный выбор

    Критическая идея: место хранения влияет на безопасность.

    Частые варианты:

  • В памяти (in-memory)
  • В localStorage
  • В cookie
  • Практически безопасный и популярный компромисс:

  • refresh token: HttpOnly cookie (недоступен JS)
  • access token: в памяти (например, в модуле tokenStore или в состоянии приложения)
  • Почему localStorage рискован:

  • при XSS злоумышленник может прочитать localStorage и украсть токены
  • Почему cookie требуют внимания:

  • если cookie используется для аутентификации, появляется риск CSRF, и сервер обычно включает SameSite, CSRF-токены или другие меры
  • Как выглядит защищённый запрос

    Вариант с Bearer access token

    Вариант с cookie-сессией

    fetchWithAuth с защитой от циклов и с дедупликацией refresh

    Что здесь решено:

  • 401 приводит к попытке refresh
  • refresh выполняется один раз для множества параллельных запросов
  • исходный запрос повторяется только один раз, что снижает риск бесконечного цикла
  • AbortController остаётся применимым через init.signal
  • Как и в прошлых статьях курса, не забывайте: fetch не выбрасывает ошибку на 401/500. Поэтому удобно после fetchWithAuth централизованно проверять response.ok и превращать неуспех в throw.

    Реализация на Axios: интерцепторы

    Axios часто выбирают, когда хочется централизовать аутентификацию и обработку ошибок.

    Документация:

  • Axios: Interceptors
  • Axios: Cancellation
  • Пример с интерцепторами и дедупликацией refresh:

    Обратите внимание на флаг original._retry:

  • он не даёт повторять refresh по кругу, если проблема не в истёкшем токене
  • Как это сочетается с состояниями загрузки, ретраями и отменой

    Аутентификация накладывает дополнительные правила на логику из предыдущих статей.

  • Если вы делаете ретраи GET, не смешивайте их с refresh бесконтрольно: refresh сам по себе уже похож на ретрай механизма авторизации.
  • AbortController стоит использовать и для refresh-запросов, если refresh запускается из компонента. Но если refresh централизован в клиенте, чаще отменяют только запросы UI, а refresh оставляют завершиться.
  • При гонках запросов (например, пользователь быстро меняет страницу) refresh-дедупликация через refreshPromise предотвращает лишнюю нагрузку.
  • Типовые ошибки и как их избежать

  • Пытаться refresh-ить запрос refresh: добавляйте явную защиту.
  • Не ограничивать повтор: один 401 должен приводить максимум к одной попытке обновления токена и одному повтору исходного запроса.
  • Хранить refresh token в localStorage: при XSS это критичный риск. Если возможно, храните refresh token в HttpOnly cookie.
  • Забывать про credentials: "include" или withCredentials: true, когда refresh опирается на cookie.
  • Считать 401 “обычной ошибкой сети”: это сигнал про аутентификацию, и UI-реакция обычно другая (например, переход на экран входа).
  • Практический чек-лист

  • Выберите модель: cookie-сессии или access/refresh токены.
  • Решите, где хранить refresh token и access token.
  • Сделайте единый слой API-клиента, чтобы не копировать Authorization и обработку 401 по компонентам.
  • Защититесь от параллельных refresh и от бесконечных циклов.
  • Убедитесь, что отмена запросов и защита от гонок сохраняются.
  • Что дальше

    После аутентификации логично развивать слой работы с API в сторону:

  • унифицированного формата ошибок для UI
  • кэширования и дедупликации запросов
  • более продвинутой архитектуры data fetching (переиспользуемые хуки или специализированные библиотеки)
  • 7. Архитектура: сервисы API, кеширование и React Query

    Архитектура: сервисы API, кеширование и React Query

    Как это продолжает курс

    Ранее в курсе мы строили работу с API на уровне компонентов:

  • делали запросы через fetch и Axios
  • запускали их в useEffect
  • управляли loading / error / data
  • отменяли запросы (AbortController), делали дебаунс и защищались от гонок
  • добавляли аутентификацию, refresh и повтор запроса после 401
  • Эти подходы полностью рабочие, но по мере роста приложения появляются архитектурные проблемы:

  • один и тот же запрос копируется в нескольких компонентах
  • сложно централизованно кешировать данные и избегать лишних запросов
  • ручное управление loading / error / retry разрастается
  • актуализация данных после мутаций (создание, изменение, удаление) становится трудной
  • В этой статье мы разберём, как обычно строят слой доступа к данным в React-приложениях и почему для кеширования и синхронизации состояния сервера с UI часто используют React Query (сейчас библиотека называется TanStack Query).

    Ссылки:

  • TanStack Query: Overview
  • TanStack Query: React Reference
  • !Общая карта слоёв: где живут запросы, где кеш и как UI получает данные

    Что такое кеширование в UI и какие задачи оно решает

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

    Кеширование помогает:

  • уменьшить количество запросов
  • сделать интерфейс быстрее при возврате на уже открытые экраны
  • разделять данные между компонентами без ручного пробрасывания
  • аккуратно обновлять данные после мутаций через инвалидацию или обновление кеша
  • Важно различать:

  • состояние сервера (server state): данные, которые принадлежат серверу и могут меняться независимо от UI (список товаров, профиль, корзина)
  • состояние клиента (client state): локальные данные интерфейса (открыт ли модал, значение инпута, выбранная вкладка)
  • React Query специализируется именно на server state.

    Базовая архитектура без React Query: сервисы API + собственный стейт

    Зачем выделять слой сервисов

    Если держать fetch/axios прямо в компонентах, быстро появляются проблемы:

  • сложно переиспользовать запросы
  • трудно обеспечить единообразную обработку ошибок
  • токены и заголовки начинают дублироваться
  • Поэтому часто делают слой api/ с функциями, которые:

  • принимают параметры запроса
  • возвращают данные в удобном виде
  • внутри используют общий HTTP-клиент
  • Пример: HTTP-клиент + сервисы

    httpClient может быть вашим fetchWithAuth из прошлой статьи (с авто-refresh), или axios instance с интерцепторами.

    Что здесь решено по сравнению с ручным useEffect:

  • состояние загрузки и ошибки ведётся автоматически
  • запрос отменяется через signal при размонтировании и при смене ключа
  • кеширование и повторное использование результата появляются автоматически
  • Документация:

  • TanStack Query: useQuery
  • Управление “свежестью” данных: staleTime и gcTime

    В React Query данные в кеше могут быть:

  • fresh (свежие): библиотека считает, что их можно использовать без рефетча
  • stale (устаревшие): данные можно показать, но при необходимости будет рефетч
  • Ключевые параметры:

  • staleTime: сколько миллисекунд данные считаются свежими
  • gcTime: сколько миллисекунд неиспользуемые данные живут в кеше до удаления
  • Пример:

    Практическая интерпретация:

  • staleTime: 30_000: в течение 30 секунд повторный заход на экран не обязан делать новый запрос
  • gcTime: 5 минут: если пользователь ушёл со страницы, данные ещё могут пригодиться при быстром возврате
  • Документация:

  • TanStack Query: Important Defaults
  • Кеширование списков и пагинация: keepPreviousData

    Для списков часто важно не “мигать” UI при смене страницы.

    Идея:

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

  • TanStack Query: Placeholder Query Data
  • useMutation: изменения данных и синхронизация кеша

    Базовая мутация и инвалидация

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

    Что означает invalidateQueries:

  • мы помечаем кеш как устаревший
  • при следующем использовании или по внутренним правилам будет выполнен рефетч
  • Документация:

  • TanStack Query: useMutation
  • TanStack Query: Query Invalidation
  • Обновление кеша без рефетча

    Иногда эффективнее обновить кеш сразу:

    Когда это уместно:

  • API возвращает актуальную сущность после обновления
  • вы хотите мгновенно синхронизировать UI, не дожидаясь нового GET
  • Документация:

  • TanStack Query: QueryClient setQueryData
  • Оптимистические обновления

    Оптимистическое обновление — это приём, когда UI обновляется до ответа сервера, как будто запрос уже успешен.

    Сценарий:

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

    Здесь:

  • onMutate сохраняет предыдущее значение и сразу обновляет кеш
  • onError откатывает кеш, если запрос не прошёл
  • onSettled в любом случае синхронизирует с сервером
  • Документация:

  • TanStack Query: Optimistic Updates
  • Как React Query связан с темами отмены, гонок и ретраев

    Отмена запросов

    В прошлых статьях мы вручную использовали AbortController. В React Query отмена происходит через signal, который вы получаете в queryFn.

    Практическое правило:

  • если ваш API-слой умеет принимать { signal }, передавайте его всегда
  • Гонки данных

    Гонки часто возникают при быстром изменении параметров. В React Query:

  • разные queryKey считаются разными запросами
  • устаревшие запросы обычно не “перетирают” данные другого ключа
  • Но если вы используете один и тот же ключ для разных параметров, вы сами создадите проблему. Поэтому проектирование queryKey — критичная часть архитектуры.

    Ретраи

    React Query умеет делать автоматические ретраи.

    Важно:

  • ретраи безопаснее для GET
  • для POST/PUT/PATCH/DELETE ретраи требуют осторожности и чаще делаются на уровне бизнес-логики
  • Документация:

  • TanStack Query: Query Retries
  • Как сочетать React Query и аутентификацию с refresh

    React Query не заменяет ваш HTTP-клиент. Рекомендуемая архитектура:

  • HTTP-клиент отвечает за:
  • - добавление Authorization - refresh по 401 - единый формат ошибок
  • React Query отвечает за:
  • - кеширование - состояния запросов - инвалидацию и синхронизацию

    Практический совет:

  • если refresh провалился, централизованно очищайте токены и переводите приложение в состояние “не залогинен”
  • ошибки 401/403 полезно различать: 401 часто означает проблему аутентификации, 403 — отсутствие прав
  • Когда React Query не нужен

    React Query полезен, когда у вас много server state и нужна синхронизация. Но иногда он избыточен:

  • приложение маленькое и запросов 2–3
  • данные не переиспользуются и не кешируются
  • требования по кешированию отсутствуют
  • Тем не менее, даже в небольшом проекте слой api/ с сервисами обычно оправдан.

    Мини-чеклист архитектуры для реального проекта

  • Вынесите HTTP-логику в единый клиент: fetchWithAuth или axios instance.
  • Вынесите эндпоинты в api/* сервисы с понятными функциями.
  • Для server state используйте React Query:
  • - продумайте queryKey - задайте staleTime осознанно - после мутаций делайте invalidateQueries или setQueryData
  • Не забывайте про отмену: передавайте signal в запросы.
  • Что дальше

    После появления сервисного слоя и React Query вы можете развивать архитектуру дальше:

  • унификация ошибок для UI (например, преобразование ошибок API в структурированный формат)
  • предзагрузка данных перед переходом на страницу
  • управление доступом и сценариями разлогина при 401
  • тестирование API-слоя и хуков
  • React Query закрывает большую часть рутины работы с данными и позволяет сосредоточиться на UX и бизнес-логике, сохраняя при этом контроль над сетевым уровнем, который мы строили в прошлых статьях.