React Lab Survival: FSD, Auth и React Query

Практический курс для подготовки к лабораторной работе, охватывающий архитектуру FSD, сложную JWT-авторизацию и управление данными. Вы научитесь строить масштабируемые приложения с валидацией форм и оптимизированными запросами к API.

1. Архитектура Feature-Sliced Design и настройка HTTP-клиента

Архитектура Feature-Sliced Design и настройка HTTP-клиента

Добро пожаловать в курс React Lab Survival. Если вы читаете это, значит, перед вами стоит задача сдать лабораторную работу, которая звучит как список требований к коммерческому проекту: авторизация с двумя токенами, сложная валидация, React Query и архитектура FSD. Звучит страшно, но мы разберем этого слона по частям.

Эта статья — фундамент. Мы не будем писать код «как попало», чтобы потом переписывать его в ночь перед дедлайном. Мы сразу настроим проект так, чтобы преподавателю было не к чему придраться в плане структуры.

Почему Feature-Sliced Design?

В маленьких проектах (например, To-Do List) мы привыкли делить код по типам файлов: папка components, папка hooks, папка pages. Это называется File-Type Architecture. Но когда проект растет, компоненты начинают путаться, импорты превращаются в паутину, и понять, какая кнопка к какой логике относится, становится невозможно.

Feature-Sliced Design (FSD) — это архитектурная методология, которая делит код не по типам файлов, а по бизнес-ценности. Она заставляет вас отвечать на вопрос: «Для чего нужен этот код?», а не «Что это за файл?».

Слои FSD

FSD делит приложение на слои. Это строгая иерархия: нижние слои не могут использовать верхние. Это спасает от циклических зависимостей.

!Иерархия слоев FSD: от переиспользуемой базы (Shared) до инициализации приложения (App).

Разберем слои снизу вверх (от самых независимых к самым сложным):

  • Shared (Общее)
  • Самый нижний слой. Здесь лежит код, который не знает ничего о бизнесе вашего приложения. Это UI-кит (кнопки, инпуты), утилиты, конфигурация API, типы данных. Пример: Button, Input, axiosInstance.

  • Entities (Сущности)
  • Бизнес-единицы, с которыми работает ваше приложение. Они содержат данные и отображение этих данных, но не содержат логику взаимодействия. Пример: User (карточка профиля), Product (карточка товара).

  • Features (Фичи)
  • Слой действий. Здесь происходит взаимодействие пользователя с сущностями. Это то, что приносит ценность. Пример: AuthByEmail (форма входа), AddToCart (кнопка добавления в корзину).

  • Widgets (Виджеты)
  • Самостоятельные блоки страницы, объединяющие сущности и фичи. Пример: Header (логотип + меню + профиль), ProductList (список товаров с фильтрами).

  • Pages (Страницы)
  • Композиция виджетов, фич и сущностей для конкретного маршрута. Пример: LoginPage, CatalogPage.

  • App (Приложение)
  • Инициализация. Здесь подключаются провайдеры, роутер, глобальные стили. Пример: App.tsx, providers, styles.

    Структура проекта для лабораторной

    Исходя из требований вашей лабы (Auth + CRUD), структура папок в src будет выглядеть примерно так:

    > Главное правило FSD: модуль может импортировать только из модулей, находящихся на слоях ниже него.

    Настройка HTTP-клиента (Axios)

    Для работы с сетью мы будем использовать библиотеку Axios. Она является стандартом де-факто благодаря встроенной поддержке перехватчиков (interceptors), которые критически важны для реализации авторизации с обновлением токенов.

    Установка

    Создание инстанса

    В FSD конфигурация API относится к слою Shared. Создадим файл src/shared/api/api.ts.

    Мы не будем использовать глобальный объект axios, а создадим настроенный экземпляр (instance). Это позволит нам задать базовый URL и настройки один раз.

    Зачем нужны Interceptors (Перехватчики)?

    В требованиях к лабе указано: «Авторизация с двумя токенами и автообновлением access». Это значит, что:

  • К каждому запросу нужно автоматически добавлять Access Token.
  • Если сервер ответил ошибкой 401 (Unauthorized), нужно попробовать обновить токены и повторить запрос.
  • Делать это вручную в каждом компоненте — безумие. Для этого и нужны перехватчики.

    #### Request Interceptor (Перехватчик запроса)

    Он срабатывает перед тем, как запрос улетит на сервер. Здесь мы добавляем токен.

    #### Response Interceptor (Перехватчик ответа)

    Он срабатывает, когда ответ пришел от сервера. Здесь мы ловим ошибки. Полную логику обновления токенов (Refresh Flow) мы реализуем в следующей статье, посвященной авторизации, но каркас подготовим сейчас.

    Интеграция с переменными окружения

    Никогда не хардкодьте адреса API. Создайте в корне проекта файл .env:

    И добавьте .env в .gitignore, чтобы не слить секретные ключи (если они будут) в репозиторий.

    Резюме

    Мы заложили прочный фундамент:

  • FSD спасет нас от превращения кода в спагетти по мере роста функционала.
  • Слои четко определяют, где должна лежать логика, а где — UI.
  • Axios Instance в слое Shared готов к работе и автоматически подставляет токены.
  • В следующей статье мы реализуем полноценную систему авторизации, подключим React Query и заставим этот механизм работать как часы.

    2. Реализация JWT авторизации: Access/Refresh токены и автоматическое обновление

    Реализация JWT авторизации: Access/Refresh токены и автоматическое обновление

    Приветствую, выжившие. В прошлой статье мы заложили фундамент архитектуры FSD и настроили базовый экземпляр Axios. Сегодня мы займемся самой «страшной» частью лабораторной работы — авторизацией с двумя токенами.

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

    Теория: Зачем нам два токена?

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

    Стандарт JWT (JSON Web Token) в безопасной реализации подразумевает использование пары токенов:

  • Access Token (Токен доступа)
  • * Живет: Мало (15–30 минут). * Использование: Отправляется в заголовке Authorization каждого запроса. * Хранение: В памяти JS или localStorage (для лабы допустимо).

  • Refresh Token (Токен обновления)
  • * Живет: Долго (2 недели – месяц). * Использование: Отправляется только на один эндпоинт: /auth/refresh. * Цель: Получить новую пару Access/Refresh токенов, когда старый Access протух.

    !Диаграмма процесса автоматического обновления токена при ошибке 401.

    Хранение токенов в FSD

    Согласно Feature-Sliced Design, работа с токенами — это низкоуровневая логика, которая может понадобиться везде. Однако, сами данные о сессии (залогинен ли юзер) относятся к слою Entities (сущность Session или User). А вот функции для работы с localStorage лучше вынести в Shared.

    Создадим файл утилит для токенов:

    Реализация Interceptor (Перехватчика)

    Вернемся к нашему файлу src/shared/api/api.ts. В прошлой статье мы написали заготовку. Теперь наполним её реальной логикой.

    Проблема параллельных запросов

    Представьте ситуацию: ваше приложение загружается и делает одновременно 5 запросов (профиль, список товаров, корзина, новости, настройки). У всех запросов протухший токен.

    Если не предусмотреть защиту, произойдет следующее:

  • Все 5 запросов получат 401.
  • Все 5 перехватчиков попытаются отправить запрос /refresh.
  • Сервер обработает первый, выдаст новые токены и сделает старый Refresh токен невалидным (обычная практика безопасности — Refresh Token Rotation).
  • Остальные 4 запроса на обновление упадут с ошибкой, и пользователя выкинет из системы.
  • Решение: Нам нужен механизм очереди (или мьютекса). Если обновление уже идет, остальные запросы должны ждать его завершения.

    Полный код api.ts

    Разбор ключевых моментов

    Очередь failedQueue

    Это массив объектов, содержащих resolve и reject от промисов. Когда приходит 401, но isRefreshing === true, мы не делаем запрос, а создаем новый Promise и кладем его методы управления в массив. Когда обновление завершится, мы вызовем processQueue, который разрезолвит (выполнит) все ожидающие промисы с новым токеном.

    Флаг _isRetry

    Мы расширяем стандартный конфиг Axios свойством _isRetry. Это защита от бесконечного цикла. Если мы обновили токен, повторили запрос, и он снова вернул 401 (например, сервер глючит или токен сразу протух), мы не должны пытаться обновлять его вечно. Мы просто выкидываем ошибку.

    Интеграция с React Query

    Самое прекрасное в этом подходе то, что React Query даже не узнает о том, что произошла ошибка 401.

    Для React Query это выглядит так:

  • Он вызвал функцию-фетчер (которая использует наш api).
  • Функция «подумала» чуть дольше обычного (пока шло обновление токенов).
  • Функция вернула успешные данные.
  • Вам не нужно писать никакой дополнительной логики в useQuery или useMutation. Вся грязь спрятана внутри Axios.

    Обработка выхода (Logout)

    В блоке catch внутри интерцептора есть комментарий про редирект. В FSD слое Shared мы не имеем доступа к роутеру приложения (так как роутер находится выше, в слое App или используется в Pages).

    Как быть? Есть два пути:

  • Простой (для лабы): window.location.href = '/login'. Это жесткая перезагрузка страницы, но это работает надежно.
  • Архитектурный: Использовать паттерн "Event Bus" или подписаться на изменения стора вне React-компонентов. Но для лабораторной работы первого варианта более чем достаточно.
  • Резюме

    Мы реализовали надежный механизм авторизации: * Безопасность: Access токены живут недолго. * UX: Пользователь не замечает обновления токенов. * Стабильность: Очередь запросов защищает от гонки (Race Condition). * Архитектура: Логика инкапсулирована в shared/api и не засоряет компоненты.

    Теперь, когда наш HTTP-клиент стал «умным», мы готовы переходить к созданию форм и валидации данных, чем и займемся в следующей статье.

    3. Валидация форм: связка React Hook Form и Zod

    Валидация форм: связка React Hook Form и Zod

    Добро пожаловать обратно в лабораторию. В прошлых статьях мы построили надежный HTTP-клиент и разобрались с токенами. Теперь у нас есть «труба» для данных, но что именно мы будем в неё отправлять?

    Если вы когда-либо писали формы на чистом React, вы знаете эту боль: бесконечные useState, ручные проверки if (email.includes('@')), управление состоянием isSubmitting и errors. В коммерческой разработке (и в вашей лабораторной) так делать нельзя. Код превратится в спагетти быстрее, чем вы скажете «дедлайн».

    Сегодня мы освоим «Священный Грааль» React-форм: React Hook Form (RHF) для управления состоянием и Zod для валидации данных.

    Почему именно эта связка?

    React Hook Form

    Это библиотека для работы с формами, которая ставит во главу угла производительность. В отличие от контролируемых компонентов (где каждое нажатие клавиши вызывает ререндер всей формы), RHF использует неконтролируемые компоненты и прямую работу с DOM через ссылки (refs). Это значит, что ваша форма будет летать даже на слабых устройствах.

    Zod

    Это библиотека для описания схем и валидации TypeScript-first. Она позволяет описать правила данных один раз и автоматически получить:
  • Логику валидации.
  • Типы TypeScript (через z.infer).
  • Вам больше не нужно вручную писать интерфейсы для форм. Типы выводятся из правил валидации. Это исключает ситуацию, когда валидация проверяет одно, а TypeScript ожидает другое.

    !Диаграмма, показывающая поток данных слева направо. Слева блок 'User Input' (Ввод пользователя), стрелка ведет в блок 'React Hook Form' (Управление состоянием). Оттуда стрелка вниз в блок 'Zod Resolver' (Связующее звено), который обращается к блоку 'Zod Schema' (Правила валидации). Если валидация успешна, стрелка идет в 'onSubmit', если ошибка — возвращается в 'Form Errors'.

    Установка зависимостей

    Нам понадобятся сама библиотека форм, Zod и специальный «переходник» (resolver), который позволяет им дружить.

    Архитектура FSD: Куда класть формы?

    В Feature-Sliced Design важно разделять ответственность:

  • Shared: Здесь лежат переиспользуемые UI-компоненты (Input, Button) и общие правила валидации (например, регулярка для телефона).
  • Features: Здесь лежат конкретные формы (LoginForm, RegisterForm) и их специфичные схемы валидации.
  • Шаг 1: Создание умного Input (Shared)

    Чтобы RHF мог управлять инпутом, ему нужен доступ к ref этого элемента. В React для передачи ref в кастомный компонент используется forwardRef.

    Создадим src/shared/ui/Input/Input.tsx:

    Шаг 2: Описание схемы валидации (Feature)

    Допустим, мы делаем форму входа. Это фича Auth. Создадим схему в src/features/auth/model/login-schema.ts.

    Обратите внимание на z.infer. Нам не нужно писать interface LoginSchema { email: string... }. Zod делает это за нас. Если вы измените .email() на .number(), TypeScript сразу подсветит ошибки во всем проекте.

    Шаг 3: Сборка формы (Feature)

    Теперь соберем всё вместе в компоненте LoginForm. Мы будем использовать хук useForm и zodResolver.

    Файл: src/features/auth/ui/LoginForm/LoginForm.tsx

    Разбор ключевых моментов

    Функция register

    Функция register('fieldName') возвращает набор пропсов: onChange, onBlur, ref и name. Распыляя их через {...register('email')}, мы внедряем логику RHF внутрь нашего компонента Input. Именно поэтому нам нужен был forwardRef в компоненте Input — RHF должен иметь доступ к нативному DOM-элементу.

    Обработка ошибок

    Объект errors автоматически заполняется сообщениями из нашей Zod-схемы. Если пользователь введет "invalid-email", в errors.email.message появится текст "Некорректный формат email". Нам не нужно писать никакой логики отображения — просто передать строку в проп error.

    handleSubmit

    Это обертка высокого порядка. Она:
  • Предотвращает стандартное поведение формы (e.preventDefault()).
  • Запускает валидацию всех полей.
  • Если есть ошибки — прерывает выполнение и обновляет объект errors.
  • Если ошибок нет — вызывает вашу функцию onSubmit и передает ей уже типизированные и проверенные данные.
  • Продвинутая валидация

    Zod позволяет делать очень сложные проверки. Например, проверка совпадения паролей при регистрации:

    Резюме

    Мы внедрили мощный инструмент, который сэкономит вам часы отладки.

  • Zod гарантирует, что данные соответствуют ожиданиям, и дает бесплатные типы.
  • React Hook Form управляет состоянием формы без лишних ререндеров.
  • FSD помогает разложить всё по полочкам: UI отдельно, логика отдельно.
  • Теперь у нас есть всё для создания полноценного приложения: архитектура, сеть, авторизация и формы. В следующей статье мы объединим это всё, добавив React Query для реализации CRUD-операций с умным кэшированием и состоянием загрузки.

    4. Магия React Query: реализация CRUD операций и кэширование

    Магия React Query: реализация CRUD операций и кэширование

    Добро пожаловать на финишную прямую подготовки к лабораторной работе. У нас уже есть настроенный HTTP-клиент с авторизацией (Axios + Interceptors) и мощные формы (React Hook Form + Zod). Остался последний, но самый важный элемент — данные.

    В старые времена (года 3 назад) мы писали useEffect, вручную создавали состояния isLoading, isError, data, боролись с гонкой запросов и лишними ререндерами. Сегодня мы будем использовать TanStack Query (React Query). Это библиотека, которая превращает работу с серверным состоянием в магию.

    Почему React Query?

    React Query решает три главные проблемы, с которыми вы столкнетесь в лабе:

  • Кэширование: Если вы загрузили список товаров, перешли на другую страницу и вернулись — данные покажутся мгновенно из кэша, а фоновый запрос обновит их, если нужно.
  • Дедупликация: Если два компонента одновременно просят одни и те же данные, уйдет только один запрос на сервер.
  • Синхронизация: После создания или удаления элемента список обновляется автоматически. Больше никаких items.filter(...) вручную.
  • Настройка провайдера

    В архитектуре FSD глобальные провайдеры живут в слое App. Нам нужно обернуть все приложение в QueryClientProvider.

    Создадим файл настройки клиента. Обычно это делают в shared/api/query-client.ts или прямо в app/providers.

    Теперь подключим его в App.tsx:

    Чтение данных (Read) с useQuery

    Допустим, у нас есть сущность Product. В FSD запросы к API для сущностей лежат в entities/product/api.

    Для получения данных используется хук useQuery. Ему нужны две вещи:

  • Query Key (Ключ запроса): Уникальный массив, по которому кэшируются данные. Например: ['products'] или ['products', id].
  • Query Fn (Функция запроса): Асинхронная функция, возвращающая данные. Здесь мы используем наш настроенный api из прошлой статьи.
  • Теперь в компоненте (например, widgets/ProductList) использование выглядит элементарно:

    Изменение данных (CUD) с useMutation

    Для создания, обновления и удаления используется хук useMutation. В отличие от useQuery, он не запускается автоматически при рендере. Он возвращает функцию mutate, которую вы вызываете сами (например, по клику).

    Самое важное здесь — инвалидация кэша. Когда мы создаем новый продукт, наш кэш списка продуктов (['products']) становится устаревшим. Мы должны сказать React Query: «Эй, данные изменились, перезапроси их».

    !Схема цикла обновления данных: Мутация -> Успех -> Инвалидация ключа -> Автоматический перезапрос списка -> Обновление интерфейса.

    Реализуем хук создания продукта:

    Использование в форме

    Объединим это с формой из прошлой статьи:

    Оптимистичные обновления (Бонус для отличников)

    Иногда мы не хотим ждать ответа сервера, чтобы обновить UI. Например, при лайке поста. Мы хотим, чтобы сердечко загорелось мгновенно.

    React Query позволяет менять кэш вручную до того, как придет ответ сервера. Если сервер вернет ошибку, изменения откатятся.

    Для лабораторной работы это не обязательно, но преподаватели очень любят такие детали.

    Резюме по курсу

    Мы прошли полный путь создания надежного React-приложения:

  • FSD: Разложили код по полочкам (shared, entities, features), чтобы не запутаться.
  • Axios: Настроили interceptors для автоматического добавления токенов и их обновления (Refresh Token Flow).
  • Forms: Подключили React Hook Form и Zod для валидации без боли.
  • React Query: Реализовали работу с данными, которая автоматически обновляет интерфейс и кэширует результаты.
  • Теперь у вас есть полный набор инструментов («Survival Kit»), чтобы сдать лабораторную работу и, что важнее, писать качественный код на реальных проектах. Удачи в кодинге!

    5. Улучшение UX: обработка ошибок, лоадеры и стилизация интерфейса

    Улучшение UX: обработка ошибок, лоадеры и стилизация интерфейса

    Поздравляю, коллеги. Мы прошли долгий путь. У нас есть архитектура FSD, настроенная авторизация с обновлением токенов, мощная валидация форм и кэширование данных. Ваше приложение работает, но... будем честны, выглядит оно пока «так себе».

    Когда пользователь нажимает кнопку «Войти», ничего не происходит две секунды, а потом страница резко меняется. Если сервер упал, пользователь видит белый экран или бесконечную загрузку. Для лабораторной работы (и для жизни) это недопустимо.

    Сегодня мы наведем лоск: добавим индикацию загрузки, красивые уведомления об ошибках и защитим приложение от падений. Это та самая «вишенка на торте», которая отличает поделку студента от работы инженера.

    Глобальная обработка ошибок (Notifications)

    Вспомните наш api.ts из второй статьи. Мы научили его обновлять токены, но что, если сервер вернет ошибку 500? Или 403? Сейчас мы просто пишем console.error, который видит только разработчик.

    Пользователь должен знать, что происходит. Для этого мы внедрим систему Toast-уведомлений (всплывающие плашки).

    Выбор инструмента

    Для React существует множество библиотек: react-toastify, sonner, react-hot-toast. Для лабы возьмем классику — react-toastify.

    Настройка в FSD

    Провайдер уведомлений должен находиться на слое App, так как он нужен всему приложению.

    В src/app/providers/ToastProvider.tsx:

    Не забудьте подключить его в App.tsx.

    Интеграция с Axios

    Теперь самое интересное. Мы не будем вызывать toast.error в каждом компоненте вручную. Мы сделаем это централизованно в нашем HTTP-клиенте.

    Вернемся в src/shared/api/api.ts и модифицируем Response Interceptor:

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

    Индикация загрузки (Loaders & Skeletons)

    Ничто так не бесит, как нажатие на кнопку без обратной связи. Пользователь начинает кликать яростно, отправляя 10 запросов на сервер.

    Спиннеры (Spinners)

    Спиннеры хороши для кнопок и небольших действий. Создадим переиспользуемый компонент в shared/ui/Spinner.

    Используем его в кнопке (которую мы, надеюсь, тоже вынесли в shared/ui/Button):

    Теперь в формах (из статьи 3) мы просто передаем проп isLoading:

    Скелетоны (Skeletons)

    Когда загружается целая страница или список карточек, спиннер выглядит одиноко. Лучше использовать Skeleton Loading — серые прямоугольники, имитирующие контент. Это создает ощущение, что данные «почти загрузились».

    !Слева — обычный спиннер, справа — Skeleton UI, который воспринимается пользователем как более быстрый.

    Пример простого скелетона на Tailwind:

    Использование в списке товаров (React Query):

    Error Boundary: Защита от белого экрана

    Иногда ошибки случаются не в сети, а в коде рендера (например, попытка прочитать свойство undefined). В React это приводит к падению всего приложения — пользователь видит просто белый экран.

    Чтобы этого избежать, используем паттерн Error Boundary (Предохранитель). Удобнее всего взять библиотеку react-error-boundary.

    Обернем наше приложение в src/app/App.tsx:

    Теперь, если какой-то виджет сломается, пользователь увидит кнопку «Попробовать снова», а не пустоту.

    Стилизация и Layout (FSD подход)

    В требованиях сказано «CSS не страшный». Это значит, что не нужно делать дизайн уровня Apple, но отступы должны быть ровными, а цвета — приятными.

    Layout (Макет)

    В FSD макеты страниц часто выносят в слой Shared (если они совсем глупые) или Widgets (если содержат логику, например, Sidebar).

    Создадим src/shared/ui/Layout/MainLayout.tsx:

    И применим его в роутере (src/app/router.tsx):

    Чек-лист готовности к сдаче

    Мы закончили курс. Давайте проверим, что у вас есть в арсенале для защиты лабораторной:

  • Архитектура: Проект разбит на shared, entities, features, widgets, pages. Импорты идут только снизу вверх.
  • Сеть: Axios Instance с перехватчиками. Токены обновляются автоматически, ошибки показываются в Toast.
  • Данные: React Query кэширует запросы. Нет лишних useEffect.
  • Формы: React Hook Form + Zod. Валидация работает, типы строгие.
  • UX: Есть лоадеры, скелетоны и защита от падений.
  • Этого набора достаточно, чтобы не просто сдать лабу, но и получить респект от преподавателя. Вы не просто написали код, который «вроде работает», вы создали поддерживаемую инженерную систему.

    Удачи на защите! И помните: лучший код — это тот, который легко читать и сложно сломать.