С момента своего первого релиза в 2015 году Redux использовался и продолжает использоваться на множестве клиентских приложений. Несмотря на все достоинства, которые предоставляет данное решение (предсказуемое управление состоянием, удобная отладка с помощью Redux DevTools и др.), некоторые разработчики сетуют на излишнее количество “шаблонного кода” при реализации даже самого просто функционала и предпочитают альтернативные инструменты для управления состоянием в клиентских приложениях.  

Чтобы избежать чрезмерного количества кода при работе с Redux, разработчики применяли различные соглашения (например, ducks-modular-redux), а также создавали свои решения, представляющие собой абстрактный слой над Redux’ом (например, redux-crud, свои оболочки над библиотекой и прочее).   

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

Получение данных с сервера и последующая их визуализация – типовые задачи веб-приложений. Как правило, веб-приложения также вносят изменения в эти данные, отправляют измененные данные на сервер, хранят закэшированные данные на клиенте и при необходимости обновляют их. Помимо этого, они также выполняют множество других задач, например:  

  1. Отображение статуса загрузки на UI (Spinners) 

  2. Дедубликация запросов 

  3. Оптимистические обновления UI  

  4. Контроль кэша приложения по мере взаимодействия пользователя с UI  

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

RTK Query – мощный инструмент для загрузки и кэширования данных. Уменьшая количество написанного разработчиком кода для загрузки и кэширования данных, RTK Query призван упростить наиболее типовые кейсы при взаимодействии с API веб-приложениями.  

Подход, используемый в RTK Query, был вдохновлен такими решениями, как Apollo Client, React Query и другими.  

Ключевые особенности RTK Query:

  • RTK Query представляет собой абстракцию над Redux Toolkit. Под капотом он использует createSlice и createAsyncThunk, которые предоставляет API Redux Toolkit. 

  • В RTQ Query взаимодействие с API задается с помощью endpoint, которые определены в момент инициализации API (метод createApi, о котором пойдет речь ниже), в отличии от решений, подобных React Query или SWR. 

  • RTK Query автоматически создает хуки, исходя из заданных эндпоинтов. Данные хуки могут быть использованы непосредственно в React компонентах для загрузки/отображения/изменения данных. Механизм взаимодействия с API инкапсулирован. 

  • RTK Query поддерживает кэширование из коробки. 

  • Позволяет решить проблему дедубликации запросов, например, если два компонента на одной странице совершают один и тот же запрос к API, выполнен будет лишь один запрос.  

RTK Query на практике  

Если Redux Toolkit установлен в вашем приложении, RTK Query уже доступен, т. к. он входит в состав Redux Toolkit.  

Шаг 1: создание API Slice 

Начнем с создания с так называемого “API Slice”, в котором определим базовый URL сервера и эндпоинты, с которым нужно будет взаимодействовать.  

Определим три главных параметра:  

  1. reducerPath. Уникальный ключ, который будет добавлен в store

  2. baseQuery. Параметр baseQuery отвечает за непосредственное взаимодействие с API. В состав RTK Query входит инструмент под названием fetchBaseQuery, представляющий собой легковесную обертку над fetch, подходящий для большинства операций по работе с API

  3. endPoints. Это набор взаимодействий с API. Существует два вида endpoint: query и mutation 

// Need to use the React-specific entry point to import createApi 

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' 

 

// Define a service using a base URL and expected endpoints 

export const starWarsApi = createApi({ 

  reducerPath: 'starWars', 

  baseQuery: fetchBaseQuery({ baseUrl: "https://swapi.dev/api" }), 

  endpoints: (builder) => ({ 

    // Define endpoints here 

  }) 

}) 

В примере выше в качестве baseUrl мы использовали популярный Star Wars API.  

Шаг 2: добавление endpoints в API Slice 

Как было указано выше, существует два вида endpoint: query и mutation. Зададим два query endpoint’а:  

endpoints: (builder) => ({ 

 getFilms: builder.query({ 

     query: () => `/films?format=json` 

  }), 

 getFilmById: builder.query({ 

   query: (filmId) => `/films/${filmId}?format=json` 

  }) 

}) 

В коде выше были добавлены два endpoint: 

  1. getFilms. Получение списка всех фильмов; 

  1. getFilmById. Получение одного фильма по его ID. В данном случае filmId представляет собой query параметр; при необходимости набор параметров можно расширить.  

Шаг 3: Экспорт сгенерированных хуков  

Самое интересное начинается здесь. Для каждого endpoint, объявленного выше, RTK Query автоматически генерирует хуки, которые могут быть использован в React компонентах для загрузки/изменения данных. Рассмотрим это на следующем примере: 

Как видно на изображении, starWarsApi после инициализации содержит в себе сгенерированные хуки. В зависимости от типов endpoint, название хуков будет содержать в себе либо query, либо mutation.  

Это просто, не так ли?  

Финальная версия Star Wars API Slice выглядит следующим образом:  

// Need to use the React-specific entry point to import createApi 

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' 

 

// Define a service using a base URL and expected endpoints 

export const starWarsApi = createApi({ 

  reducerPath: 'starWars', 

  baseQuery: fetchBaseQuery({ baseUrl: "https://swapi.dev/api" }), 

  endpoints: (builder) => ({ 

    getFilms: builder.query({ 

      query: () => `/films?format=json` 

    }), 

    getFilmById: builder.query({ 

      query: (filmId) => `/films/${filmId}?format=json` 

    }) 

  }), 

}) 

 

// Export hooks for usage in functional components, which are 

// auto-generated based on the defined endpoints 

export const { useGetFilmsQuery, useGetFilmByIdQuery } = starWarsApi 
 

Шаг 4: добавление API сервиса в Redux store  

Метод createApi генерирует reducer, который должен быть добавлен в store. Также для обеспечения возможностей RTK Query (кэширование, инвалидация, polling и др.) необходимо добавить middleware как на примере ниже:  

import { configureStore } from '@reduxjs/toolkit' 

import { setupListeners } from '@reduxjs/toolkit/query' 

import { starWarsApi } from './services/starWarsApi' 

 

export const store = configureStore({ 

  reducer: { 

    // Add the generated reducer as a specific top-level slice 

    [starWarsApi.reducerPath]: starWarsApi.reducer, 

  }, 

  // Adding the api middleware enables caching, invalidation, polling, 

  // and other useful features of `rtk-query`. 

  middleware: (getDefaultMiddleware) => 

    getDefaultMiddleware().concat(starWarsApi.middleware), 

}) 

 

// optional, but required for refetchOnFocus/refetchOnReconnect behaviors 

// see `setupListeners` docs - takes an optional callback as the 2nd arg for customization 

setupListeners(store.dispatch) 

Настройка на этом завершена. Далее рассмотрим использование сгенерированных хуков в React компонентах.  

Шаг 5: использование RTK Query хуков в компонентах  

import { useGetFilmsQuery } from '../reduxStore/services/starWarsApi'; 

 

const FilmsList = () => { 

  const { data, isLoading, error } = useGetFilmsQuery(); 

 

  return ( 

    <div> 

      <h3>Star Wars Movies</h3> 

      {error ? ( 

        <>Oh no, there was an error</> 

      ) : isLoading ? ( 

        <>Loading...</> 

      ) : data ? ( 

        <div> 

          {data.results.map(movie => ( 

            <section item key={movie.episode_id} xs={4}> 

              <h2>{movie.title}</h2> 

              <p>{movie.opening_crawl}</p> 

            </section> 

          ))} 

        </div> 

      ) : null} 

    </div> 

  ) 

} 

 

export default FilmsList; 

Рассмотрим описанный код выше: 

  • Сначала мы просто импортировали хук useGetFilmsQuery 

  • При вызове хука useGetFilmsQuery будет автоматически производиться вызов к API для получения всех фильмов. Хук в свою очередь возвращает не только вышеуказанные значения, но и также ряд других полезных, таких как isFetching,  isError и другие. 

Выводы

  1. Теперь не приходится создавать action creators для каждого запроса 

  2. Нет нужды создавать множество reducers 

  3. Обработка состояний запросов (isFetching, isError и др.) теперь производится автоматически 

  4. В React компонентах не нужно вызывать метод dispatch или использовать селекторы для взаимодействия со store  

В результате количество написанного кода становится меньше, а его восприятие заметно улучшилось.