Урок 11: RTK Query — полная замена `createAsyncThunk` для работы с API

Приветствую на одиннадцатом уроке нашего большого курса по Redux и Redux Toolkit. Сегодня мы перешагнем через один из самых важных рубежей в освоении управления состоянием. Мы будем говорить о RTK Query.

Если до этого мы вручную создавали асинхронные запросы с помощью createAsyncThunk, заботились о состояниях загрузки и ошибок, то сегодня я покажу вам инструмент, который перевернет ваше представление о работе с серверными данными. Готовьтесь, после этого урока вы вряд ли захотите возвращаться к старому способу.

Представьте себе мир, где вам больше не нужно писать редьюсеры для pendingfulfilled и rejected состояний. Мир, где кэширование данных, их повторная загрузка (refetching) и инвалидация (обновление актуальных данных) происходят автоматически, без единой строчки дополнительного кода. Этот мир не утопия, а реальность, которую дарит нам RTK Query.

RTK Query это не отдельная библиотека, а основной инструмент, встроенный прямо в @reduxjs/toolkit. Он создан специально для решения задач по загрузке, кэшированию и синхронизации данных с сервера. Он не просто заменяет createAsyncThunk. Он вытесняет его, предлагая абстракцию более высокого уровня, которая избавляет нас от тонн шаблонного кода.

Глубокое погружение в RTK Query

В основе всего в RTK Query лежит функция createApi. Именно она описывает, как мы взаимодействуем с нашим бэкенд API. Она является источником истины для всех конечных точек нашего API.

createApi принимает объект конфигурации, главными полями которого являются:

  1. reducerPath. Уникальный ключ, под которым состояние нашего API будет жить в общем сторе Redux. Классический вариант – 'api'.

  2. baseQuery. Функция, которая определяет, как выполнять базовые запросы. Чаще всего мы используем fetchBaseQuery – обертку вокруг стандартного fetch, которая уже настроена для работы с RTK Query.

  3. tagTypes. Массив «типов тегов». Это ключевая концепция для инвалидации кэша. Теги – это как метки на данных. Мы можем пометить, что определенные данные относятся к типу 'User' или 'Post' и позже, выполнив мутацию (например, добавление новой сущности), мы можем сказать: «Инвалидируй все данные с тегом 'User'«, чтобы они автоматически перезагрузились.

  4. endpoints. Колбэк-функция, которая получает builder и возвращает объект с описанием всех наших конечных точек, как для получения данных, так и для их изменения.

Давайте посмотрим на это в коде. Создадим файл features/users/usersApi.js.

javascript
// features/users/usersApi.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

// Определяем наш API-слайс
export const usersApi = createApi({
  // Ключ, под которым будет храниться состояние этого API в сторе
  reducerPath: 'usersApi',
  // Базовая настройка для всех запросов
  baseQuery: fetchBaseQuery({
    // Базовый URL для всех запросов
    baseUrl: 'https://jsonplaceholder.typicode.com/',
  }),
  // Типы тегов для инвалидации кэша
  tagTypes: ['User'],
  // Конечные точки (endpoints) нашего API
  endpoints: (builder) => ({
    // Эндпоинт для получения всех пользователей
    getUsers: builder.query({
      query: () => '/users', // Относительный путь к endpoint
      // Указываем, что этот запрос предоставляет данные с тегом 'User'
      providesTags: ['User'],
    }),
    // Эндпоинт для получения одного пользователя по ID
    getUserById: builder.query({
      query: (id) => `/users/${id}`,
      // Здесь мы используем функцию для точного указания, какие данные предоставляет запрос
      providesTags: (result, error, id) => [{ type: 'User', id }],
    }),
    // Эндпоинт для создания нового пользователя (мутация)
    createUser: builder.mutation({
      query: (newUser) => ({
        url: '/users',
        method: 'POST',
        body: newUser,
      }),
      // Эта мутация инвалидирует тег 'User', вызывая перезапрос всех запросов, которые зависят от него
      invalidatesTags: ['User'],
    }),
    // Эндпоинт для обновления пользователя
    updateUser: builder.mutation({
      query: ({ id, ...patch }) => ({
        url: `/users/${id}`,
        method: 'PUT',
        body: patch,
      }),
      // Точная инвалидация: инвалидируем только конкретного пользователя по ID
      invalidatesTags: (result, error, { id }) => [{ type: 'User', id }],
    }),
    // Эндпоинт для удаления пользователя
    deleteUser: builder.mutation({
      query: (id) => ({
        url: `/users/${id}`,
        method: 'DELETE',
      }),
      // При удалении инвалидируем весь список, так как он изменился
      invalidatesTags: ['User'],
    }),
  }),
})

// RTK Query автоматически генерирует хуки для каждого эндпоинта
// Название формируется так: use + ИмяЭндпоинта + Query/Mutation
export const {
  useGetUsersQuery,
  useGetUserByIdQuery,
  useCreateUserMutation,
  useUpdateUserMutation,
  useDeleteUserMutation,
} = usersApi

Мы только что описали всю логику взаимодействия с API для сущности «Пользователь». Обратите внимание, что мы нигде не писали createAsyncThunk, не создавали слайс, не описывали начальное состояние и не писали редьюсеры для обработки состояний загрузки. Вся эта суть инкапсулирована внутри RTK Query.

Автоматическая генерация хуков: useGetQuery, usePostMutation

Самое основное в createApi, это автоматическая генерация React-хуков. Взгляните на последние строки предыдущего примера. Мы экспортируем хуки, которые были созданы для нас.

  • Для запросов на получение данных генерируются хуки с суффиксом QueryuseGetUsersQueryuseGetUserByIdQuery.

  • Для мутаций изменения данных, генерируются хуки с суффиксом MutationuseCreateUserMutationuseUpdateUserMutation.

Давайте посмотрим, как использовать эти хуки в компоненте. Создадим UsersList.jsx.

jsx
// features/users/UsersList.jsx
import React from 'react';
import { useGetUsersQuery, useDeleteUserMutation } from './usersApi';

export const UsersList = () => {
  // Используем хук запроса. Он автоматически вызовет запрос при монтировании компонента.
  const {
    data: users,          // Данные, полученные с сервера (при успешном запросе)
    error,                // Объект ошибки, если запрос провалился
    isLoading,            // true, когда запрос выполняется в первый раз
    isFetching,           // true, когда запрос выполняется (включая повторные)
    isSuccess,            // true, если запрос завершился успешно
    isError,              // true, если запрос завершился ошибкой
    refetch,              // Функция для принудительного повторного запроса
  } = useGetUsersQuery(/* можно передать параметры, если эндпоинт их требует */);

  // Хук мутации возвращает массив, где первый элемент - это функция-триггер, а второй - объект с состоянием мутации
  const [deleteUser, { isLoading: isDeleting }] = useDeleteUserMutation();

  const handleDeleteUser = async (id) => {
    try {
      // Вызываем функцию мутации и передаем данные (в данном случае ID)
      await deleteUser(id).unwrap();
      // Благодаря invalidatesTags: ['User'], хук useGetUsersQuery автоматически выполнит повторный запрос!
      // Данные списка пользователей обновятся. Нам не нужно диспатчить никакие экшены.
    } catch (err) {
      console.error('Failed to delete the user: ', err);
    }
  };

  // Рендерим состояние загрузки
  if (isLoading) return <div>Загрузка пользователей...</div>;
  // Рендерим состояние ошибки
  if (isError) return <div>Ошибка: {error.message}</div>;

  return (
    <div>
      <h2>Список пользователей</h2>
      <button onClick={refetch} disabled={isFetching}>
        {isFetching ? 'Обновляем...' : 'Обновить вручную'}
      </button>
      {isSuccess && users && (
        <ul>
          {users.map((user) => (
            <li key={user.id}>
              {user.name} ({user.email})
              <button
                onClick={() => handleDeleteUser(user.id)}
                disabled={isDeleting}
              >
                Удалить
              </button>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
};

Посмотрите, насколько чистым и декларативным стал наш компонент. Хук useGetUsersQuery сам управляет жизненным циклом запроса. Он предоставляет все необходимые флаги (isLoadingisErrordata), а также функцию refetch для повторного выполнения запроса.

Хук мутации useDeleteUserMutation возвращает функцию-триггер (deleteUser) и объект с состоянием самой мутации (например, isLoading под другим именем, чтобы избежать конфликтов). Вызов deleteUser(id) инициирует запрос на удаление. Метод .unwrap() позволяет работать с результатом как с обычным Promise, что очень удобно для обработки успеха и ошибок.

Кэширование, инвалидация и повторная загрузка

Основное свойство RTK Query, это продвинутое, автоматическое кэширование по принципу «cache-and-invalidate» (кэшируй и инвалидируй).

Как это работает?

  • Кэширование. Когда вы вызываете хук запроса (например, useGetUsersQuery), RTK Query сначала проверяет, есть ли уже в кэше актуальные данные для этого запроса. Данные кэшируются по комбинации endpoint name + args (аргументам запроса). По умолчанию, если данные есть и им меньше 60 секунд, они считаются актуальными и возвращаются из кэша мгновенно, без сетевого запроса. Это называется «условный повторный запрос».

  • Инвалидация. Это механизм пометки кэшированных данных как устаревших. Когда данные становятся устаревшими, RTK Query автоматически выполняет за ними повторный запрос. Самый частый способ инвалидации, через мутации. В нашем примере в createUser мы указали invalidatesTags: ['User']. Это означает: «После успешного выполнения этой мутации, пометь все данные с тегом 'User' как устаревшие». Все активные хуки (например, useGetUsersQuery на странице), которые зависят от тега 'User', увидят это и автоматически выполнят повторный запрос для обновления данных.

  • Повторная загрузка. Это сам процесс повторного запроса данных. Он может быть запущен разными способами:

    • Автоматически, при инвалидации тега.

    • Вручную, с помощью функции refetch, которую возвращает хук запроса.

    • При повторном монтировании компонента (если данные устарели).

    • При восстановлении фокуса на окне браузера (это поведение можно включить в настройках API).

    • По интервалу (polling), с помощью опции pollingInterval в хуке.

Благодаря этой системе вы получаете согласованные данные во всем вашем приложении практически без усилий. Добавили пост? Список постов на другой странице мгновенно обновится. Удалили пользователя? Он исчезнет из всех списков.

Переписываем логику загрузки пользователей

Давайте закрепим материал, выполнив практическое задание. Напомню, в прошлых уроках мы загружали пользователей с помощью createAsyncThunk. Теперь мы сделаем это «правильным» способом.

Шаг 1: Создайте и настройте API

Создайте файл src/features/users/usersApi.js и поместите в него код, который я показывал выше с использованием createApi.

Шаг 2: Подключите API редьюсер к стору

Нам нужно добавить редьюсер, который сгенерировал usersApi, в наш корневой стор. RTK Query сам создает этот редьюсер.

javascript
// app/store.js
import { configureStore } from '@reduxjs/toolkit'
import { usersApi } from '../features/users/usersApi' // Импортируем наш API

export const store = configureStore({
  reducer: {
    // Подключаем редьюсер из usersApi.
    // Ключ 'usersApi' должен совпадать с reducerPath, который мы указали в createApi.
    [usersApi.reducerPath]: usersApi.reducer,
  },
  // Добавляем middleware от usersApi для обработки кэширования, инвалидации, polling и т.д.
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(usersApi.middleware),
})

Это критически важный шаг! Без подключения usersApi.middleware такие функции, как кэширование, инвалидация и автоматический refetching, работать не будут.

Шаг 3: Используйте хуки в компонентах

Создайте или отредактируйте компонент UsersList, как показано в примере выше.

Шаг 4: Создайте компонент для добавления пользователя

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

jsx
// features/users/AddUser.jsx
import React, { useState } from 'react';
import { useCreateUserMutation } from './usersApi';

export const AddUser = () => {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  // Хук для создания пользователя
  const [createUser, { isLoading }] = useCreateUserMutation();

  const handleSubmit = async (e) => {
    e.preventDefault();
    if (!name.trim() || !email.trim()) return;

    try {
      // Отправляем нового пользователя на сервер
      await createUser({ name, email, username: name }).unwrap();
      // Если мутация успешна, RTK Query автоматически инвалидирует тег 'User'
      // Это заставит useGetUsersQuery в компоненте UsersList перезапросить данные!
      
      // Очищаем форму
      setName('');
      setEmail('');
    } catch (err) {
      console.error('Ошибка при создании пользователя:', err);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <h3>Добавить нового пользователя</h3>
      <div>
        <label>
          Имя:
          <input
            type="text"
            value={name}
            onChange={(e) => setName(e.target.value)}
            disabled={isLoading}
          />
        </label>
      </div>
      <div>
        <label>
          Email:
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            disabled={isLoading}
          />
        </label>
      </div>
      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Добавляем...' : 'Добавить пользователя'}
      </button>
    </form>
  );
};

Шаг 5: Соберите все вместе в App.js

jsx
// App.js
import React from 'react';
import { UsersList } from './features/users/UsersList';
import { AddUser } from './features/users/AddUser';

function App() {
  return (
    <div className="App">
      <h1>Redux Toolkit Query Example</h1>
      <AddUser />
      <UsersList />
    </div>
  );
}

export default App;

Теперь запустите ваше приложение. Вы увидите, что:

  1. Список пользователей загружается автоматически.

  2. При добавлении нового пользователя (через форму) он «волшебным образом» появляется в списке без вашего прямого вмешательства. Это сработала инвалидация!

  3. При удалении пользователя список также обновляется.

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

Сегодня мы совершили гигантский скачок вперед. RTK Query это принципиально иной, более декларативный и основной подход к работе с серверным состоянием.

Ключевые выводы:

  1. Меньше кода. Мы полностью избавились от ручного создания слайсов, экшенов и редьюсеров для асинхронных операций.

  2. Автоматизация. Кэширование, инвалидация, загрузка и ошибки управляются автоматически.

  3. Согласованность данных. Механизм тегов гарантирует, что данные в разных частях приложения остаются актуальными.

  4. Отличный DevEx. Автогенерация хуков и их богатый API делают разработку невероятно удобной.

RTK Query настолько хорош, что для большинства приложений он полностью вытесняет необходимость использования createAsyncThunk и ручного управления серверным состоянием. Я настоятельно рекомендую использовать его для всех новых проектов, где требуется работа с API. Попробуйте реализовать обновление пользователя, настройте опцию pollingInterval для периодического обновления списка, изучите продвинутые сценарии с тегами.

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

До встречи в следующем уроке, где мы будем учиться работать с несколькими слайсами и комбинировать их состояния.

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

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

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