Урок 10: Структура больших проектов (Feature-Based Folders и RTK Query)

Это уже десятый урок нашего курса по Redux и Redux Toolkit. Если вы следовали всем предыдущим урокам, то у вас уже есть прочная база, вы знаете основы Redux, умеете создавать слайсы, работать с асинхронными операциями и подключать всё это к React-компонентам.

Но по мере роста нашего приложения, его кодовая база тоже растет. Если не позаботиться о структуре заранее, очень скоро мы получим хаос из файлов, в которых будет сложно ориентироваться. Сегодня мы поговорим о том, как поддерживать порядок в большом проекте и познакомимся с одним из самых мощных инструментов в экосистеме Redux Toolkit, это RTK Query.

Представьте, что вы строите дом. На начальном этапе все инструменты и материалы можно сложить в одну кучу. Но когда вы начинаете возводить стены, прокладывать коммуникации и заниматься отделкой, такая «куча» превращается в кошмар. Чтобы найти отвертку, придется перекопать половину стройплощадки.

То же самое происходит и с кодом. Пока наш проект это просто счётчик и список задач, мы можем хранить все слайсы, компоненты и стили в одной-двух папках. Но что, если мы добавляем аутентификацию, профили пользователей, систему комментариев, ленту новостей? Файлов становится десятки, если не сотни.

Без четкой структуры мы столкнемся с проблемами:

  1. Сложность навигации. Трудно найти, где находится логика для определенной функциональности.

  2. Низкая сопровождаемость. Внесение изменений в одну фичу может случайно задеть другую.

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

  4. Сложности с тестированием. Запутанные зависимости между модулями.

Чтобы избежать этого, мы будем использовать подход, основанный на фичах (features) или предметных областях (domain-based).

Feature-Based структура папок

Идея проста, организовывать код не по его типу (все компоненты в /components, все редьюсеры в /reducers), а по его функциональному назначению.

Давайте сравним два подхода.

1. Классическая структура (по типам файлов)

text
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 структура

text
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 создадим папки appfeatures и shared.

  • Перенесем хранилище и корневой компонент.

    1. Файл store.js переместим в src/app/.

    2. Файл App.jsx также переместим в src/app/. Не забудем обновить импорты внутри App.jsx и в главном main.jsx.

  • Создадим фичи. Внутри src/features/ создадим три папки: countertodosusers.

  • Распределим код по фичам.

    1. В src/features/counter/ переносим Counter.jsx и counterSlice.js.

    2. В src/features/todos/ переносим TodoList.jsxTodoItem.jsx и todosSlice.js.

    3. В src/features/users/ переносим UserList.jsxUserCard.jsx и usersSlice.js (который мы создавали с помощью createAsyncThunk).

  • Обновим импорты. Это самая кропотливая часть. Нужно проверить и поправить все пути к импортируемым модулям.

    1. В src/app/store.js обновим пути к слайсам:

      javascript
      // Было: import counterReducer from '../counterSlice';
      // Стало:
      import counterReducer from '../features/counter/counterSlice';
      import todosReducer from '../features/todos/todosSlice';
      import usersReducer from '../features/users/usersSlice';
    2. В src/app/App.jsx обновим пути к компонентам:

      javascript
      // Было: import Counter from './Counter';
      // Стало:
      import Counter from '../features/counter/Counter';
      import TodoList from '../features/todos/TodoList';
      import UserList from '../features/users/UserList';
    3. Внутри самих фич, если один файл импортирует другой (например, компонент импортирует селектор из слайса), путь, скорее всего, останется коротким: import { selectAllTodos } from './todosSlice';.

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

Знакомство с RTK Query

В прошлый раз мы использовали createAsyncThunk для загрузки списка пользователей. Это работающий подход, но он требует от нас написания немало кода: экшены для всех стадий запроса (pending, fulfilled, rejected), обработка этих экшенов в extraReducers, управление состоянием загрузки и ошибок вручную.

RTK Query это гибкое решение для работы с данными, встроенное прямо в Redux Toolkit. Оно предназначено для упрощения выполнения запросов к API и управления кэшированием данных на клиенте. По сути, это отдельная библиотека внутри RTK, которая полностью заменяет необходимость в createAsyncThunkcreateSlice и ручном управлении состоянием загрузки для API-запросов.

Какие проблемы решает RTK Query?

  1. Избыточность кода. Генерирует редьюсеры, экшены и селекторы автоматически.

  2. Кэширование. Автоматически кэширует результаты запросов. Повторный запрос на тот же endpoint с теми же параметрами не будет выполнен, а данные возьмутся из кэша.

  3. Инвалидация кэша. Позволяет помечать данные как «устаревшие», чтобы автоматически перезапросить их (например, после добавления новой записи).

  4. Фоновое обновление. Может автоматически перезапрашивать данные при повторном подключении компонента к странице или при установке фокуса на окно браузера.

  5. Оптимистичные обновления. Поддержка обновления UI до подтверждения от сервера.

  6. Пагинация и бесконечный скроллинг. Встроенная поддержка сложных сценариев работы с данными.

Основные концепции RTK Query

  • API Slice (Слайс API). Центральное место конфигурации. Создается с помощью функции createApi(). Здесь мы определяем все endpoints (конечные точки) нашего API.

  • Endpoints (Конечные точки). Описывают, как приложение взаимодействует с сервером. Бывают двух типов:

    1. Query (Запрос). Для получения данных (GET).

    2. Mutation (Мутация). Для изменения данных (POST, PUT, PATCH, DELETE).

  • Сгенерированные хуки. Для каждого endpoint’а RTK Query автоматически генерирует React-хуки (например, useGetUsersQuery для запроса и useAddUserMutation для мутации). Эти хуки управляют fetching’ом, подпиской компонента на данные и предоставляют всю необходимую информацию: dataerrorisLoadingisFetchingrefetch и др.

Переписываем фичу пользователей с использованием RTK Query

Давайте на практике заменим наш createAsyncThunk в фиче пользователей на элегантное решение от RTK Query.

  1. Создадим новый файл для API. Внутри src/features/users/ создадим файл usersApi.js.

  2. Определим наш API Slice.

    javascript
    // 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
  3. Подключим API к хранилищу. Теперь нам нужно добавить редьюсер от нашего usersApi в корневой редьюсер хранилища. RTK Query сам управляет своим состоянием.

    javascript
    // 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),
    })
  4. Обновим компонент UserList. Теперь мы можем полностью избавиться от логики, связанной с useDispatchuseSelector и нашим старым слайсом. Весь state management будет происходить под капотом у хука useGetUsersQuery.

    javascript
    // 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:

    1. Создайте API slice для работы с задачами (todos), используя JSONPlaceholder (https://jsonplaceholder.typicode.com/todos).

    2. Определите endpoint getTodos для получения списка задач.

    3. Подключите созданный API slice к хранилищу.

    4. Перепишите компонент TodoList, чтобы он использовал хук useGetTodosQuery вместо старого слайса.

  • Исследование. Попробуйте добавить в usersApi endpoint для получения одного пользователя по ID (getUserById). Создайте новый компонент UserDetails, который будет принимать userId через props и использовать хук useGetUserByIdQuery(userId) для отображения подробной информации о пользователе.

Итоги 10-го урока

Сегодня мы проделали огромную работу по подготовке нашего приложения к масштабированию. Мы узнали:

  1. Почему структура проекта критически важна для поддержания порядка в растущем приложении.

  2. Как организовать код по функциональности (feature-based), создав папки features/counterfeatures/todos и features/users.

  3. Что такое RTK Query и какие проблемы она решает. Избыточность кода, кэширование, инвалидация и многое другое.

  4. Как создать API slice с помощью createApi и fetchBaseQuery.

  5. Как подключить RTK Query к хранилищу, добавив его редьюсер и middleware.

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

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

Полный курс с уроками по Redux и Redux Toolkit для начинающих: https://max-gabov.ru/redux-dlya-nachinaushih

Поделиться статьей:
Поддержать автора блога

Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.

Персональные рекомендации
Оставить комментарий