Установка и настройка окружения

RTK Query — библиотека для получения, кэширования, синхронизации и управления серверными данными в приложениях на Javascript. Библиотека входит в экосистему Redux Toolkit и решает типичные проблемы клиент-серверного взаимодействия:

  • выполнение HTTP-запросов;
  • хранение результатов запросов;
  • кэширование данных;
  • повторное использование результатов;
  • автоматическое обновление данных;
  • управление состояниями загрузки;
  • обработка ошибок;
  • инвалидация кэша;
  • оптимистичные обновления.

RTK Query значительно сокращает объём шаблонного кода по сравнению с ручным использованием fetch, axios или классического Redux.


Требования к окружению

Для работы RTK Query требуется современное Javascript-окружение.

Минимальный набор:

  • Node.js;
  • npm или yarn;
  • React;
  • Redux Toolkit.

Наиболее распространённый стек:

Node.js
React
Redux Toolkit
RTK Query
TypeScript (опционально)

Проверка установленного Node.js:

node -v

Проверка npm:

npm -v

Создание проекта

Создание приложения через Vite

Современный способ инициализации React-приложения — использование Vite.

Создание проекта:

npm create vite@latest rtk-query-app

Выбор параметров:

Framework: React
Variant: JavaScript

Переход в каталог проекта:

cd rtk-query-app

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

npm install

Запуск dev-сервера:

npm run dev

Установка Redux Toolkit

RTK Query встроен в Redux Toolkit, поэтому отдельная установка основной библиотеки RTK Query не требуется.

Установка:

npm install @reduxjs/toolkit react-redux

После установки становятся доступны:

  • Redux Toolkit;
  • React bindings;
  • RTK Query;
  • createApi;
  • fetchBaseQuery.

Структура проекта

Базовая структура приложения:

src/
├── app/
│   └── store.js
├── services/
│   └── api.js
├── features/
├── components/
├── pages/
├── hooks/
├── App.jsx
└── main.jsx

Назначение каталогов

app/

Содержит глобальную конфигурацию Redux Store.

services/

Содержит API-сервисы RTK Query.

features/

Модули бизнес-логики.

components/

Переиспользуемые React-компоненты.

pages/

Компоненты страниц.

hooks/

Кастомные React-hooks.


Настройка Redux Store

Создание файла:

src/app/store.js

Базовая конфигурация:

import { configureStore } from '@reduxjs/toolkit'
import { api } from '../services/api'

export const store = configureStore({
  reducer: {
    [api.reducerPath]: api.reducer,
  },

  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(api.middleware),
})

Разбор configureStore

reducer

Подключает reducer RTK Query в глобальное состояние Redux.

[api.reducerPath]: api.reducer

По умолчанию reducerPath:

'api'

Итоговое состояние:

state.api

middleware

RTK Query использует middleware для:

  • выполнения запросов;
  • отслеживания подписок;
  • кэширования;
  • polling;
  • автоматического refetch;
  • инвалидации тегов.

Подключение middleware обязательно:

getDefaultMiddleware().concat(api.middleware)

Без middleware библиотека работать корректно не будет.


Создание API-сервиса

Создание файла:

src/services/api.js

Базовый пример:

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

export const api = createApi({
  reducerPath: 'api',

  baseQuery: fetchBaseQuery({
    baseUrl: 'https://jsonplaceholder.typicode.com/',
  }),

  endpoints: () => ({}),
})

Разбор createApi

createApi

Главная функция RTK Query для создания API-слоя.

Она автоматически генерирует:

  • hooks;
  • reducers;
  • actions;
  • selectors;
  • middleware integration.

reducerPath

Определяет имя раздела в Redux Store.

reducerPath: 'api'

Состояние будет храниться:

state.api

baseQuery

Базовый механизм выполнения запросов.

Наиболее популярный вариант:

fetchBaseQuery

Это обёртка над стандартным fetch.


baseUrl

Базовый URL для всех запросов:

baseUrl: 'https://jsonplaceholder.typicode.com/'

Теперь endpoint:

/posts

будет автоматически преобразован в:

https://jsonplaceholder.typicode.com/posts

endpoints

Описание всех API-endpoints приложения.

Пока объект пуст:

endpoints: () => ({})

Endpoints будут добавляться позже.


Подключение Provider

Файл:

src/main.jsx

Настройка:

import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'

import App from './App'
import { store } from './app/store'

ReactDOM.createRoot(document.getElementById('root')).render(
  <Provider store={store}>
    <App />
  </Provider>
)

Назначение Provider

Компонент Provider из React Redux делает Redux Store доступным всему React-приложению.

Без Provider:

  • hooks RTK Query не смогут получить store;
  • запросы работать не будут;
  • появятся runtime errors.

Подключение RTK Query Hooks

После создания endpoints RTK Query автоматически генерирует hooks.

Пример:

export const {
  useGetPostsQuery,
} = api

Эти hooks используются внутри React-компонентов.


Первый endpoint

Расширение api.js:

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

export const api = createApi({
  reducerPath: 'api',

  baseQuery: fetchBaseQuery({
    baseUrl: 'https://jsonplaceholder.typicode.com/',
  }),

  endpoints: (builder) => ({
    getPosts: builder.query({
      query: () => 'posts',
    }),
  }),
})

export const {
  useGetPostsQuery,
} = api

Разбор builder.query

builder.query

Создаёт GET-запрос.

builder.query({
  query: () => 'posts',
})

Запрос:

GET /posts

query

Возвращает URL endpoint.

query: () => 'posts'

RTK Query автоматически объединяет:

baseUrl + query()

Использование query hook

Пример компонента:

import { useGetPostsQuery } from '../services/api'

export default function Posts() {
  const {
    data,
    error,
    isLoading,
  } = useGetPostsQuery()

  if (isLoading) {
    return <div>Loading...</div>
  }

  if (error) {
    return <div>Error</div>
  }

  return (
    <ul>
      {data.map((post) => (
        <li key={post.id}>
          {post.title}
        </li>
      ))}
    </ul>
  )
}

Автоматически генерируемые состояния

RTK Query автоматически управляет состояниями запроса.

data

Полученные данные.

error

Ошибка запроса.

isLoading

Первая загрузка данных.

isFetching

Любой активный запрос.

isSuccess

Успешное выполнение.

isError

Ошибка выполнения.


Подключение Redux DevTools

Redux Toolkit автоматически поддерживает Redux DevTools.

Дополнительная настройка обычно не требуется.

В DevTools можно анализировать:

  • cache state;
  • endpoints;
  • subscriptions;
  • invalidation;
  • actions;
  • polling;
  • lifecycle.

Настройка alias импортов

Для крупных проектов полезно настроить абсолютные импорты.

Настройка Vite

Файл:

vite.config.js

Пример:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'

export default defineConfig({
  plugins: [react()],

  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
})

Использование alias

Вместо:

import { store } from '../. ./. ./app/store'

Можно использовать:

import { store } from '@/app/store'

Разделение API по файлам

В больших проектах API рекомендуется декомпозировать.

Пример структуры:

services/
├── api/
│   ├── baseApi.js
│   ├── postsApi.js
│   ├── usersApi.js
│   └── commentsApi.js

Базовый API

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

export const baseApi = createApi({
  reducerPath: 'api',

  baseQuery: fetchBaseQuery({
    baseUrl: 'https://api.example.com/',
  }),

  endpoints: () => ({}),
})

Инъекция endpoints

import { baseApi } from './baseApi'

export const postsApi = baseApi.injectEndpoints({
  endpoints: (builder) => ({
    getPosts: builder.query({
      query: () => 'posts',
    }),
  }),
})

Преимущества injectEndpoints

Масштабируемость

API можно разделять по доменам.

Lazy loading

Endpoints могут загружаться динамически.

Поддерживаемость

Упрощается сопровождение проекта.

Модульность

Каждый модуль содержит собственные endpoints.


Настройка fetchBaseQuery

RTK Query предоставляет множество возможностей конфигурации.

Пример:

fetchBaseQuery({
  baseUrl: 'https://api.example.com/',

  prepareHeaders: (headers, { getState }) => {
    const token = getState().auth.token

    if (token) {
      headers.set('authorization', `Bearer ${token}`)
    }

    return headers
  },
})

prepareHeaders

Позволяет динамически изменять headers перед запросом.

Чаще всего используется для:

  • JWT;
  • Bearer Token;
  • locale;
  • custom headers;
  • API keys.

Настройка timeout через custom baseQuery

fetchBaseQuery не поддерживает timeout напрямую.

Создание обёртки:

const baseQuery = fetchBaseQuery({
  baseUrl: 'https://api.example.com/',
})

const baseQueryWithTimeout = async (
  args,
  api,
  extraOptions
) => {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), 5000)
  )

  return Promise.race([
    baseQuery(args, api, extraOptions),
    timeout,
  ])
}

Настройка environment variables

.env

VITE_API_URL=https://api.example.com

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

fetchBaseQuery({
  baseUrl: import.meta.env.VITE_API_URL,
})

Настройка eslint

Установка:

npm install -D eslint

Инициализация:

npx eslint --init

Полезные правила eslint

Для React и RTK Query особенно важны:

{
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

Настройка prettier

Установка:

npm install -D prettier

Создание файла:

.prettierrc

Пример:

{
  "semi": false,
  "singleQuote": true
}

Настройка TypeScript

RTK Query особенно хорошо интегрирован с TypeScript.

Установка:

npm install -D typescript

Создание TypeScript-проекта:

npm create vite@latest

Выбор:

React + TypeScript

Типизация Store

import { configureStore } from '@reduxjs/toolkit'
import { api } from './api'

export const store = configureStore({
  reducer: {
    [api.reducerPath]: api.reducer,
  },

  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(api.middleware),
})

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

Типизация response

getPosts: builder.query<Post[], void>({
  query: () => 'posts',
})

Типизация entity

type Post = {
  id: number
  title: string
  body: string
}

Использование generated hooks в TypeScript

const { data } = useGetPostsQuery()

Тип data будет автоматически определён:

Post[] | undefined

Организация production-конфигурации

Типичная структура:

src/
├── app/
├── services/
├── shared/
├── entities/
├── features/
├── widgets/
└── pages/

Такой подход хорошо сочетается с:

  • Feature-Sliced Design;
  • domain-driven architecture;
  • modular frontend architecture.

Типичные ошибки при настройке RTK Query

Отсутствует middleware

Ошибка:

middleware: []

Правильно:

middleware: (getDefaultMiddleware) =>
  getDefaultMiddleware().concat(api.middleware)

Не подключён reducer

Ошибка:

reducer: {}

Правильно:

reducer: {
  [api.reducerPath]: api.reducer,
}

Отсутствует Provider

Без Provider hooks работать не будут.


Неправильный импорт

Нужно импортировать:

@reduxjs/toolkit/query/react

А не:

@reduxjs/toolkit/query

если используются React hooks.


Проверка работоспособности окружения

Корректно настроенный проект должен обеспечивать:

  • успешный запуск dev-сервера;
  • выполнение API-запросов;
  • появление данных в Redux DevTools;
  • автоматическое кэширование;
  • отсутствие runtime errors;
  • генерацию hooks;
  • обновление состояния при запросах.

Минимальная рабочая конфигурация

store.js

import { configureStore } from '@reduxjs/toolkit'
import { api } from '../services/api'

export const store = configureStore({
  reducer: {
    [api.reducerPath]: api.reducer,
  },

  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(api.middleware),
})

api.js

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

export const api = createApi({
  reducerPath: 'api',

  baseQuery: fetchBaseQuery({
    baseUrl: 'https://jsonplaceholder.typicode.com/',
  }),

  endpoints: (builder) => ({
    getPosts: builder.query({
      query: () => 'posts',
    }),
  }),
})

export const {
  useGetPostsQuery,
} = api

main.jsx

import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'

import App from './App'
import { store } from './app/store'

ReactDOM.createRoot(document.getElementById('root')).render(
  <Provider store={store}>
    <App />
  </Provider>
)

App.jsx

import { useGetPostsQuery } from './services/api'

export default function App() {
  const {
    data,
    isLoading,
  } = useGetPostsQuery()

  if (isLoading) {
    return <div>Loading...</div>
  }

  return (
    <div>
      {data.map((post) => (
        <div key={post.id}>
          {post.title}
        </div>
      ))}
    </div>
  )
}