Это уже десятый урок нашего курса по Redux и Redux Toolkit. Если вы следовали всем предыдущим урокам, то у вас уже есть прочная база, вы знаете основы Redux, умеете создавать слайсы, работать с асинхронными операциями и подключать всё это к React-компонентам.
Но по мере роста нашего приложения, его кодовая база тоже растет. Если не позаботиться о структуре заранее, очень скоро мы получим хаос из файлов, в которых будет сложно ориентироваться. Сегодня мы поговорим о том, как поддерживать порядок в большом проекте и познакомимся с одним из самых мощных инструментов в экосистеме Redux Toolkit, это RTK Query.
Представьте, что вы строите дом. На начальном этапе все инструменты и материалы можно сложить в одну кучу. Но когда вы начинаете возводить стены, прокладывать коммуникации и заниматься отделкой, такая «куча» превращается в кошмар. Чтобы найти отвертку, придется перекопать половину стройплощадки.
То же самое происходит и с кодом. Пока наш проект это просто счётчик и список задач, мы можем хранить все слайсы, компоненты и стили в одной-двух папках. Но что, если мы добавляем аутентификацию, профили пользователей, систему комментариев, ленту новостей? Файлов становится десятки, если не сотни.
Без четкой структуры мы столкнемся с проблемами:
-
Сложность навигации. Трудно найти, где находится логика для определенной функциональности.
-
Низкая сопровождаемость. Внесение изменений в одну фичу может случайно задеть другую.
-
Проблемы с командой. Непонятно, где разным разработчикам следует размещать свой код.
-
Сложности с тестированием. Запутанные зависимости между модулями.
Чтобы избежать этого, мы будем использовать подход, основанный на фичах (features) или предметных областях (domain-based).
Feature-Based структура папок
Идея проста, организовывать код не по его типу (все компоненты в /components, все редьюсеры в /reducers), а по его функциональному назначению.
Давайте сравним два подхода.
1. Классическая структура (по типам файлов)
src/ ├── components/ │ ├── Counter.jsx │ ├── TodoList.jsx │ └── UserList.jsx ├── reducers/ │ ├── counterSlice.js │ ├── todosSlice.js │ └── usersSlice.js ├── actions/ │ ├── counterActions.js │ ├── todosActions.js │ └── usersActions.js ├── store.js └── App.jsx
В такой структуре, чтобы понять, как работает «Счётчик», мне нужно открыть файл в components/, затем найти его редьюсер в reducers/, а потом и файл с экшенами. Все части одной фичи разбросаны по разным углам проекта.
2. Feature-Based структура
src/
├── app/
│ ├── store.js
│ └── App.jsx
├── features/
│ ├── counter/
│ │ ├── Counter.jsx
│ │ └── counterSlice.js
│ ├── todos/
│ │ ├── TodoList.jsx
│ │ ├── TodoItem.jsx
│ │ └── todosSlice.js
│ └── users/
│ ├── UserList.jsx
│ ├── UserCard.jsx
│ └── usersSlice.js
└── shared/
└── components/
└── Button.jsx
Смотрите, как теперь всё изменилось! Вся логика, связанная со счётчиком, находится в одной папке features/counter/. Компоненты, слайс, селекторы, типы (если мы используем TypeScript), всё лежит рядом. Это делает фичу самодостаточной и легко переносимой. Если нам больше не нужен счётчик, мы просто удаляем одну папку.
Папка app/ содержит глобальную конфигурацию, хранилище (store) и корневой компонент. Папка shared/ предназначена для переиспользуемых элементов, которые не относятся к конкретной фиче: общие компоненты (кнопки, инпуты), хуки, утилиты.
Реорганизуем наш проект
Давайте на практике приведем в порядок наш учебный проект. Предположим, у нас уже есть слайсы для счётчика, задач и пользователей и они все свалены в кучу в папке src/.
-
Создадим новую структуру папок. Внутри папки
srcсоздадим папкиapp,featuresиshared. -
Перенесем хранилище и корневой компонент.
-
Файл
store.jsпереместим вsrc/app/. -
Файл
App.jsxтакже переместим вsrc/app/. Не забудем обновить импорты внутриApp.jsxи в главномmain.jsx.
-
-
Создадим фичи. Внутри
src/features/создадим три папки:counter,todos,users. -
Распределим код по фичам.
-
В
src/features/counter/переносимCounter.jsxиcounterSlice.js. -
В
src/features/todos/переносимTodoList.jsx,TodoItem.jsxиtodosSlice.js. -
В
src/features/users/переносимUserList.jsx,UserCard.jsxиusersSlice.js(который мы создавали с помощьюcreateAsyncThunk).
-
-
Обновим импорты. Это самая кропотливая часть. Нужно проверить и поправить все пути к импортируемым модулям.
-
В
src/app/store.jsобновим пути к слайсам:// Было: import counterReducer from '../counterSlice'; // Стало: import counterReducer from '../features/counter/counterSlice'; import todosReducer from '../features/todos/todosSlice'; import usersReducer from '../features/users/usersSlice';
-
В
src/app/App.jsxобновим пути к компонентам:// Было: import Counter from './Counter'; // Стало: import Counter from '../features/counter/Counter'; import TodoList from '../features/todos/TodoList'; import UserList from '../features/users/UserList';
-
Внутри самих фич, если один файл импортирует другой (например, компонент импортирует селектор из слайса), путь, скорее всего, останется коротким:
import { selectAllTodos } from './todosSlice';.
-
После этой реорганизации наш проект станет намного чище и понятнее. Теперь любая работа над функциональностью «пользователи» будет вестись в одной папке, что значительно повышает продуктивность.
Знакомство с RTK Query
В прошлый раз мы использовали createAsyncThunk для загрузки списка пользователей. Это работающий подход, но он требует от нас написания немало кода: экшены для всех стадий запроса (pending, fulfilled, rejected), обработка этих экшенов в extraReducers, управление состоянием загрузки и ошибок вручную.
RTK Query это гибкое решение для работы с данными, встроенное прямо в Redux Toolkit. Оно предназначено для упрощения выполнения запросов к API и управления кэшированием данных на клиенте. По сути, это отдельная библиотека внутри RTK, которая полностью заменяет необходимость в createAsyncThunk, createSlice и ручном управлении состоянием загрузки для API-запросов.
Какие проблемы решает RTK Query?
-
Избыточность кода. Генерирует редьюсеры, экшены и селекторы автоматически.
-
Кэширование. Автоматически кэширует результаты запросов. Повторный запрос на тот же endpoint с теми же параметрами не будет выполнен, а данные возьмутся из кэша.
-
Инвалидация кэша. Позволяет помечать данные как «устаревшие», чтобы автоматически перезапросить их (например, после добавления новой записи).
-
Фоновое обновление. Может автоматически перезапрашивать данные при повторном подключении компонента к странице или при установке фокуса на окно браузера.
-
Оптимистичные обновления. Поддержка обновления UI до подтверждения от сервера.
-
Пагинация и бесконечный скроллинг. Встроенная поддержка сложных сценариев работы с данными.
Основные концепции RTK Query
-
API Slice (Слайс API). Центральное место конфигурации. Создается с помощью функции
createApi(). Здесь мы определяем все endpoints (конечные точки) нашего API. -
Endpoints (Конечные точки). Описывают, как приложение взаимодействует с сервером. Бывают двух типов:
-
Query (Запрос). Для получения данных (GET).
-
Mutation (Мутация). Для изменения данных (POST, PUT, PATCH, DELETE).
-
-
Сгенерированные хуки. Для каждого endpoint’а RTK Query автоматически генерирует React-хуки (например,
useGetUsersQueryдля запроса иuseAddUserMutationдля мутации). Эти хуки управляют fetching’ом, подпиской компонента на данные и предоставляют всю необходимую информацию:data,error,isLoading,isFetching,refetchи др.
Переписываем фичу пользователей с использованием RTK Query
Давайте на практике заменим наш createAsyncThunk в фиче пользователей на элегантное решение от RTK Query.
-
Создадим новый файл для API. Внутри
src/features/users/создадим файлusersApi.js. -
Определим наш API Slice.
// usersApi.js import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' // Создаем API slice export const usersApi = createApi({ // Уникальный ключ, который будет добавлен в путь в Redux store reducerPath: 'usersApi', // Базовая настройка для всех запросов baseQuery: fetchBaseQuery({ baseUrl: 'https://jsonplaceholder.typicode.com/', }), // Описываем endpoints (конечные точки) endpoints: (builder) => ({ // GET запрос для получения всех пользователей getUsers: builder.query({ query: () => '/users', // Относительный путь к endpoint }), // GET запрос для получения одного пользователя по ID getUserById: builder.query({ query: (userId) => `/users/${userId}`, }), // POST запрос для создания нового пользователя (мутация) // addUser: builder.mutation({ // query: (newUser) => ({ // url: '/users', // method: 'POST', // body: newUser, // }), // }), }), }) // Экспортируем автоматически сгенерированные хуки // useGetUsersQuery - это hook, который мы будем использовать в компонентах export const { useGetUsersQuery, useGetUserByIdQuery } = usersApi
-
Подключим API к хранилищу. Теперь нам нужно добавить редьюсер от нашего
usersApiв корневой редьюсер хранилища. RTK Query сам управляет своим состоянием.// src/app/store.js import { configureStore } from '@reduxjs/toolkit' import counterReducer from '../features/counter/counterSlice' import todosReducer from '../features/todos/todosSlice' // Импортируем редьюсер из нашего API slice import { usersApi } from '../features/users/usersApi' export const store = configureStore({ reducer: { counter: counterReducer, todos: todosReducer, // Добавляем редьюсер от RTK Query. // Ключ 'usersApi' должен совпадать с reducerPath, который мы указали в createApi. [usersApi.reducerPath]: usersApi.reducer, }, // Добавляем middleware от RTK Query для работы с кэшированием, инвалидацией и т.д. // Это обязательный шаг! middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(usersApi.middleware), })
-
Обновим компонент UserList. Теперь мы можем полностью избавиться от логики, связанной с
useDispatch,useSelectorи нашим старым слайсом. Весь state management будет происходить под капотом у хукаuseGetUsersQuery.// src/features/users/UserList.jsx import React from 'react' // Импортируем сгенерированный хук import { useGetUsersQuery } from './usersApi' const UserList = () => { // Используем хук. Он автоматически запустит запрос при монтировании компонента. // data - полученные с сервера данные. // error - объект ошибки, если запрос провалился. // isLoading: true пока выполняется первый запрос (нет данных в кэше). // isFetching: true пока выполняется любой запрос, включая фоновые перезапросы. const { data: users, error, isLoading, isFetching } = useGetUsersQuery() // Обработка состояний загрузки и ошибки if (isLoading) return <div>Загрузка пользователей...</div> if (error) return <div>Ошибка при загрузке: {error.status}</div> return ( <div> <h2>Список пользователей</h2> {isFetching && <div>Обновляем...</div>} {/* Показываем при фоновом обновлении */} <ul> {users?.map((user) => ( <li key={user.id}> {user.name} ({user.email}) </li> ))} </ul> </div> ) } export default UserList
Посмотрите, насколько меньше кода нам пришлось написать. RTK Query сама управляет загрузкой, ошибками, кэшированием и обновлением данных. Наш компонент стал декларативным и чистым.
Практические задачи для закрепления
-
Реорганизация. Возьмите свой текущий учебный проект и проведите его реорганизацию по feature-based структуре, как было показано выше. Убедитесь, что все импорты работают корректно и приложение запускается.
-
Внедрение RTK Query:
-
Создайте API slice для работы с задачами (todos), используя JSONPlaceholder (
https://jsonplaceholder.typicode.com/todos). -
Определите endpoint
getTodosдля получения списка задач. -
Подключите созданный API slice к хранилищу.
-
Перепишите компонент
TodoList, чтобы он использовал хукuseGetTodosQueryвместо старого слайса.
-
-
Исследование. Попробуйте добавить в
usersApiendpoint для получения одного пользователя по ID (getUserById). Создайте новый компонентUserDetails, который будет приниматьuserIdчерез props и использовать хукuseGetUserByIdQuery(userId)для отображения подробной информации о пользователе.
Итоги 10-го урока
Сегодня мы проделали огромную работу по подготовке нашего приложения к масштабированию. Мы узнали:
-
Почему структура проекта критически важна для поддержания порядка в растущем приложении.
-
Как организовать код по функциональности (feature-based), создав папки
features/counter,features/todosиfeatures/users. -
Что такое RTK Query и какие проблемы она решает. Избыточность кода, кэширование, инвалидация и многое другое.
-
Как создать API slice с помощью
createApiиfetchBaseQuery. -
Как подключить RTK Query к хранилищу, добавив его редьюсер и middleware.
-
Как использовать сгенерированные хуки в React-компонентах для простой и эффективной работы с серверными данными.
Теперь ваш проект не только функционален, но и хорошо организован, а работа с API стала настоящим удовольствием. В следующих уроках мы изучим работу с несколькими слайсами, кастомными middleware и тестированием.
Полный курс с уроками по Redux и Redux Toolkit для начинающих: https://max-gabov.ru/redux-dlya-nachinaushih
Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.


