RTK Query - работа с API

06 ноября 2026
Автор

Олег Марков

Введение

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

В отличие от написания вручную thunk-экшенов, редьюсеров и селекторов для каждого запроса, RTK Query позволяет определить всё API в одном месте и автоматически генерирует React-хуки для использования в компонентах.

Курс по теме

Пройти курс React и Redux Toolkit

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

Установка и настройка

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

npm install @reduxjs/toolkit react-redux

Создание API с помощью createApi

Основа RTK Query — функция createApi. Она принимает объект конфигурации и возвращает API-объект с хуками и утилитами.

// src/services/api.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export interface Post {
  id: number;
  title: string;
  body: string;
  userId: number;
}

export interface User {
  id: number;
  name: string;
  email: string;
}

export const postsApi = createApi({
  // Уникальный ключ для хранения в Redux store
  reducerPath: 'postsApi',
  
  // Базовая конфигурация запросов
  baseQuery: fetchBaseQuery({ 
    baseUrl: 'https://jsonplaceholder.typicode.com/' 
  }),
  
  // Теги для инвалидации кэша
  tagTypes: ['Post', 'User'],
  
  // Определение эндпоинтов
  endpoints: (builder) => ({
    getPosts: builder.query<Post[], void>({
      query: () => 'posts',
      providesTags: ['Post'],
    }),
    
    getPostById: builder.query<Post, number>({
      query: (id) => `posts/${id}`,
      providesTags: (result, error, id) => [{ type: 'Post', id }],
    }),
    
    createPost: builder.mutation<Post, Partial<Post>>({
      query: (body) => ({
        url: 'posts',
        method: 'POST',
        body,
      }),
      invalidatesTags: ['Post'],
    }),
    
    updatePost: builder.mutation<Post, Partial<Post> & Pick<Post, 'id'>>({
      query: ({ id, ...patch }) => ({
        url: `posts/${id}`,
        method: 'PATCH',
        body: patch,
      }),
      invalidatesTags: (result, error, { id }) => [{ type: 'Post', id }],
    }),
    
    deletePost: builder.mutation<{ success: boolean }, number>({
      query: (id) => ({
        url: `posts/${id}`,
        method: 'DELETE',
      }),
      invalidatesTags: ['Post'],
    }),
  }),
});

// Экспортируем автоматически сгенерированные хуки
export const {
  useGetPostsQuery,
  useGetPostByIdQuery,
  useCreatePostMutation,
  useUpdatePostMutation,
  useDeletePostMutation,
} = postsApi;

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

// src/store.ts
import { configureStore } from '@reduxjs/toolkit';
import { postsApi } from './services/api';

export const store = configureStore({
  reducer: {
    // Добавляем редьюсер API
    [postsApi.reducerPath]: postsApi.reducer,
  },
  // Добавляем middleware для кэширования, инвалидации и polling
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(postsApi.middleware),
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// src/main.tsx
import { Provider } from 'react-redux';
import { store } from './store';

function App() {
  return (
    <Provider store={store}>
      <YourApp />
    </Provider>
  );
}

Использование Query-хуков

Базовый запрос

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

function PostsList() {
  // Хук возвращает объект с состоянием запроса
  const { 
    data: posts,      // Данные ответа
    isLoading,        // true при первой загрузке
    isFetching,       // true при любой загрузке (включая refetch)
    isSuccess,        // true если запрос успешен
    isError,          // true при ошибке
    error,            // Объект ошибки
    refetch,          // Функция для ручного перезапроса
  } = useGetPostsQuery();

  if (isLoading) return <div>Загрузка...</div>;
  if (isError) return <div>Ошибка: {JSON.stringify(error)}</div>;

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

Запрос с аргументом

import { useGetPostByIdQuery } from './services/api';

function PostDetail({ postId }: { postId: number }) {
  const { data: post, isLoading } = useGetPostByIdQuery(postId);

  if (isLoading) return <div>Загрузка поста...</div>;
  
  return (
    <div>
      <h1>{post?.title}</h1>
      <p>{post?.body}</p>
    </div>
  );
}

Условные запросы

function ConditionalPost({ postId }: { postId: number | null }) {
  // Запрос не выполняется, если postId равен null
  const { data } = useGetPostByIdQuery(postId!, {
    skip: postId === null,
  });

  return <div>{data?.title}</div>;
}

Polling (автоматическое обновление)

function LiveData() {
  // Обновляем данные каждые 30 секунд
  const { data } = useGetPostsQuery(undefined, {
    pollingInterval: 30000,
  });

  return <div>{/* ... */}</div>;
}

Опции запросов

const { data } = useGetPostsQuery(undefined, {
  // Не выполнять запрос
  skip: false,
  
  // Интервал опроса в миллисекундах
  pollingInterval: 0,
  
  // Перезапрашивать при фокусе окна
  refetchOnFocus: true,
  
  // Перезапрашивать при восстановлении соединения
  refetchOnReconnect: true,
  
  // Перезапрашивать при монтировании компонента
  refetchOnMountOrArgChange: true,
  
  // Считать данные свежими N секунд после последнего запроса
  refetchOnMountOrArgChange: 60,
});

Мутации

Мутации используются для изменения данных на сервере (POST, PUT, PATCH, DELETE).

import { useCreatePostMutation, useUpdatePostMutation, useDeletePostMutation } from './services/api';

function PostForm() {
  const [createPost, { isLoading: isCreating }] = useCreatePostMutation();
  const [updatePost, { isLoading: isUpdating }] = useUpdatePostMutation();
  const [deletePost] = useDeletePostMutation();

  const handleCreate = async () => {
    try {
      // unwrap() выбрасывает ошибку если запрос завершился неудачно
      const newPost = await createPost({
        title: 'Новый пост',
        body: 'Содержимое поста',
        userId: 1,
      }).unwrap();
      
      console.log('Создан пост:', newPost);
    } catch (error) {
      console.error('Ошибка создания:', error);
    }
  };

  const handleUpdate = async (id: number) => {
    await updatePost({ id, title: 'Обновлённый заголовок' }).unwrap();
  };

  const handleDelete = async (id: number) => {
    await deletePost(id).unwrap();
  };

  return (
    <div>
      <button onClick={handleCreate} disabled={isCreating}>
        {isCreating ? 'Создаём...' : 'Создать пост'}
      </button>
    </div>
  );
}

Состояния мутации

const [createPost, mutationResult] = useCreatePostMutation();

const {
  isLoading,    // Мутация выполняется
  isSuccess,    // Мутация успешно завершена
  isError,      // Мутация завершилась ошибкой
  error,        // Объект ошибки
  data,         // Данные ответа
  reset,        // Сброс состояния мутации
} = mutationResult;

fetchBaseQuery: настройка базового запроса

fetchBaseQuery — упрощённая обёртка над fetch, поддерживающая типичные сценарии:

import { fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import type { RootState } from '../store';

const baseQuery = fetchBaseQuery({
  // Базовый URL
  baseUrl: 'https://api.example.com/',
  
  // Добавление заголовков к каждому запросу
  prepareHeaders: (headers, { getState }) => {
    // Получаем токен из Redux state
    const token = (getState() as RootState).auth.token;
    
    if (token) {
      headers.set('authorization', `Bearer ${token}`);
    }
    
    headers.set('Content-Type', 'application/json');
    return headers;
  },
  
  // Кастомный обработчик ответа
  responseHandler: 'json', // 'json' | 'text' | 'blob' | custom function
  
  // Валидация статуса (по умолчанию 200-299)
  validateStatus: (response, body) => response.status === 200 && body.success,
});

Кастомный baseQuery

Для более сложных сценариев можно написать полностью кастомный baseQuery. Например, с обновлением токена:

import { 
  fetchBaseQuery,
  BaseQueryFn,
  FetchArgs,
  FetchBaseQueryError,
} from '@reduxjs/toolkit/query/react';
import { tokenReceived, loggedOut } from './authSlice';

const baseQuery = fetchBaseQuery({
  baseUrl: 'https://api.example.com',
  prepareHeaders: (headers, { getState }) => {
    const token = (getState() as RootState).auth.accessToken;
    if (token) {
      headers.set('authorization', `Bearer ${token}`);
    }
    return headers;
  },
});

// baseQuery с автоматическим обновлением токена
export const baseQueryWithReauth: BaseQueryFn<
  string | FetchArgs,
  unknown,
  FetchBaseQueryError
> = async (args, api, extraOptions) => {
  let result = await baseQuery(args, api, extraOptions);

  if (result.error && result.error.status === 401) {
    // Пробуем обновить токен
    const refreshResult = await baseQuery(
      { url: '/auth/refresh', method: 'POST' },
      api,
      extraOptions
    );

    if (refreshResult.data) {
      // Сохраняем новый токен
      api.dispatch(tokenReceived(refreshResult.data));
      // Повторяем оригинальный запрос
      result = await baseQuery(args, api, extraOptions);
    } else {
      // Разлогиниваем пользователя
      api.dispatch(loggedOut());
    }
  }

  return result;
};

// Используем в createApi
export const api = createApi({
  baseQuery: baseQueryWithReauth,
  endpoints: (builder) => ({ /* ... */ }),
});

Теги и инвалидация кэша

Теги — ключевая концепция RTK Query для управления кэшем. Запросы "предоставляют" теги, а мутации "инвалидируют" их.

Базовые теги

const api = createApi({
  tagTypes: ['Post', 'User', 'Comment'],
  endpoints: (builder) => ({
    // Запрос предоставляет список тегов 'Post'
    getPosts: builder.query<Post[], void>({
      query: () => 'posts',
      providesTags: ['Post'],
    }),
    
    // Мутация инвалидирует все кэши с тегом 'Post'
    addPost: builder.mutation<Post, Partial<Post>>({
      query: (body) => ({ url: 'posts', method: 'POST', body }),
      invalidatesTags: ['Post'],
    }),
  }),
});

Теги с идентификаторами

Для точечной инвалидации используйте теги с ID:

const api = createApi({
  tagTypes: ['Post'],
  endpoints: (builder) => ({
    getPosts: builder.query<Post[], void>({
      query: () => 'posts',
      // Предоставляем LIST-тег и тег для каждого поста
      providesTags: (result) =>
        result
          ? [
              ...result.map(({ id }) => ({ type: 'Post' as const, id })),
              { type: 'Post', id: 'LIST' },
            ]
          : [{ type: 'Post', id: 'LIST' }],
    }),
    
    getPost: builder.query<Post, number>({
      query: (id) => `posts/${id}`,
      providesTags: (result, error, id) => [{ type: 'Post', id }],
    }),
    
    addPost: builder.mutation<Post, Partial<Post>>({
      query: (body) => ({ url: 'posts', method: 'POST', body }),
      // Инвалидируем только список, не отдельные посты
      invalidatesTags: [{ type: 'Post', id: 'LIST' }],
    }),
    
    updatePost: builder.mutation<Post, Partial<Post> & Pick<Post, 'id'>>({
      query: ({ id, ...patch }) => ({
        url: `posts/${id}`,
        method: 'PATCH',
        body: patch,
      }),
      // Инвалидируем только конкретный пост
      invalidatesTags: (result, error, { id }) => [{ type: 'Post', id }],
    }),
    
    deletePost: builder.mutation<void, number>({
      query: (id) => ({ url: `posts/${id}`, method: 'DELETE' }),
      // Инвалидируем конкретный пост и список
      invalidatesTags: (result, error, id) => [
        { type: 'Post', id },
        { type: 'Post', id: 'LIST' },
      ],
    }),
  }),
});

Трансформация данных

Для преобразования ответа сервера используйте transformResponse:

const api = createApi({
  endpoints: (builder) => ({
    getUsers: builder.query<User[], void>({
      query: () => 'users',
      // Преобразуем ответ: берём только нужные поля
      transformResponse: (response: User[]) =>
        response.map(({ id, name, email }) => ({ id, name, email })),
    }),
    
    getPaginatedPosts: builder.query<{ posts: Post[]; total: number }, number>({
      query: (page) => `posts?_page=${page}&_limit=10`,
      // Используем заголовки ответа
      transformResponse: (response: Post[], meta) => ({
        posts: response,
        total: parseInt(meta?.response?.headers.get('X-Total-Count') ?? '0'),
      }),
    }),
    
    getPost: builder.query<Post, number>({
      query: (id) => `posts/${id}`,
      // Трансформация ошибки
      transformErrorResponse: (response: { status: string | number; data: unknown }) => ({
        status: response.status,
        message: 'Ошибка загрузки поста',
      }),
    }),
  }),
});

Оптимистичные обновления

Оптимистичные обновления позволяют немедленно обновить UI до получения ответа сервера:

const api = createApi({
  endpoints: (builder) => ({
    getPosts: builder.query<Post[], void>({
      query: () => 'posts',
      providesTags: ['Post'],
    }),
    
    updatePost: builder.mutation<Post, Partial<Post> & Pick<Post, 'id'>>({
      query: ({ id, ...patch }) => ({
        url: `posts/${id}`,
        method: 'PATCH',
        body: patch,
      }),
      
      // Оптимистичное обновление
      async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
        // Немедленно обновляем кэш
        const patchResult = dispatch(
          api.util.updateQueryData('getPosts', undefined, (draft) => {
            const post = draft.find((p) => p.id === id);
            if (post) {
              Object.assign(post, patch);
            }
          })
        );
        
        try {
          // Ждём завершения запроса
          await queryFulfilled;
        } catch {
          // При ошибке откатываем изменения
          patchResult.undo();
        }
      },
    }),
  }),
});

Оптимистичное обновление отдельного элемента

updatePost: builder.mutation<Post, Partial<Post> & Pick<Post, 'id'>>({
  query: ({ id, ...patch }) => ({
    url: `posts/${id}`,
    method: 'PATCH',
    body: patch,
  }),
  
  async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
    // Обновляем кэш конкретного поста
    const patchResult = dispatch(
      api.util.updateQueryData('getPostById', id, (draft) => {
        Object.assign(draft, patch);
      })
    );
    
    try {
      await queryFulfilled;
    } catch {
      patchResult.undo();
    }
  },
}),

Пагинация

Смещение (offset-based pagination)

interface PaginatedPosts {
  posts: Post[];
  total: number;
  page: number;
}

const api = createApi({
  endpoints: (builder) => ({
    getPaginatedPosts: builder.query<PaginatedPosts, number>({
      query: (page) => `posts?_page=${page}&_limit=10`,
      transformResponse: (response: Post[], meta) => ({
        posts: response,
        total: parseInt(meta?.response?.headers.get('X-Total-Count') ?? '0'),
        page: 1,
      }),
      providesTags: (result, error, page) => [{ type: 'Post', id: `PAGE_${page}` }],
    }),
  }),
});

// Использование в компоненте
function PaginatedList() {
  const [page, setPage] = useState(1);
  const { data, isLoading, isFetching } = useGetPaginatedPostsQuery(page);

  return (
    <div>
      {isLoading ? (
        <div>Загрузка...</div>
      ) : (
        <>
          {data?.posts.map((post) => (
            <div key={post.id}>{post.title}</div>
          ))}
          <div>
            <button 
              onClick={() => setPage(p => Math.max(1, p - 1))}
              disabled={page === 1}
            >
              Назад
            </button>
            <span>Страница {page}</span>
            <button onClick={() => setPage(p => p + 1)}>
              Вперёд
            </button>
          </div>
        </>
      )}
      {isFetching && <div>Обновление...</div>}
    </div>
  );
}

Бесконечная прокрутка с merge

const api = createApi({
  endpoints: (builder) => ({
    getInfinitePosts: builder.query<Post[], number>({
      query: (page) => `posts?_page=${page}&_limit=10`,
      
      // Объединяем данные при подгрузке следующих страниц
      serializeQueryArgs: ({ endpointName }) => endpointName,
      
      merge: (currentCache, newItems) => {
        currentCache.push(...newItems);
      },
      
      forceRefetch({ currentArg, previousArg }) {
        return currentArg !== previousArg;
      },
    }),
  }),
});

function InfiniteList() {
  const [page, setPage] = useState(1);
  const { data: posts, isFetching } = useGetInfinitePostsQuery(page);

  return (
    <div>
      {posts?.map((post) => (
        <div key={post.id}>{post.title}</div>
      ))}
      <button onClick={() => setPage(p => p + 1)} disabled={isFetching}>
        Загрузить ещё
      </button>
    </div>
  );
}

TypeScript интеграция

RTK Query отлично интегрируется с TypeScript и обеспечивает полную типизацию:

Типизация эндпоинтов

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

// Типы данных
interface Todo {
  id: number;
  title: string;
  completed: boolean;
  userId: number;
}

interface CreateTodoRequest {
  title: string;
  completed?: boolean;
  userId: number;
}

interface UpdateTodoRequest {
  id: number;
  title?: string;
  completed?: boolean;
}

// Создание API с типизацией
export const todosApi = createApi({
  reducerPath: 'todosApi',
  baseQuery: fetchBaseQuery({ baseUrl: '/api/' }),
  tagTypes: ['Todo'],
  endpoints: (builder) => ({
    // builder.query<ResponseType, ArgType>
    getTodos: builder.query<Todo[], void>({
      query: () => 'todos',
      providesTags: (result) =>
        result
          ? [
              ...result.map(({ id }) => ({ type: 'Todo' as const, id })),
              { type: 'Todo', id: 'LIST' },
            ]
          : [{ type: 'Todo', id: 'LIST' }],
    }),
    
    // builder.mutation<ResponseType, ArgType>
    createTodo: builder.mutation<Todo, CreateTodoRequest>({
      query: (newTodo) => ({
        url: 'todos',
        method: 'POST',
        body: newTodo,
      }),
      invalidatesTags: [{ type: 'Todo', id: 'LIST' }],
    }),
    
    updateTodo: builder.mutation<Todo, UpdateTodoRequest>({
      query: ({ id, ...changes }) => ({
        url: `todos/${id}`,
        method: 'PATCH',
        body: changes,
      }),
      invalidatesTags: (result, error, { id }) => [{ type: 'Todo', id }],
    }),
  }),
});

export const {
  useGetTodosQuery,
  useCreateTodoMutation,
  useUpdateTodoMutation,
} = todosApi;

Типизация хранилища

// store.ts
import { configureStore } from '@reduxjs/toolkit';
import { todosApi } from './services/todosApi';
import { useDispatch, useSelector } from 'react-redux';
import type { TypedUseSelectorHook } from 'react-redux';

export const store = configureStore({
  reducer: {
    [todosApi.reducerPath]: todosApi.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(todosApi.middleware),
});

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

// Типизированные хуки
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

Инжекция эндпоинтов

Для разделения API на модули используйте injectEndpoints:

// services/baseApi.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const baseApi = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api/' }),
  tagTypes: ['Post', 'User', 'Comment'],
  endpoints: () => ({}),
});

// services/postsApi.ts
import { baseApi } from './baseApi';

export const postsApi = baseApi.injectEndpoints({
  endpoints: (builder) => ({
    getPosts: builder.query<Post[], void>({
      query: () => 'posts',
    }),
  }),
  overrideExisting: false,
});

export const { useGetPostsQuery } = postsApi;

// services/usersApi.ts
import { baseApi } from './baseApi';

export const usersApi = baseApi.injectEndpoints({
  endpoints: (builder) => ({
    getUsers: builder.query<User[], void>({
      query: () => 'users',
    }),
  }),
  overrideExisting: false,
});

export const { useGetUsersQuery } = usersApi;

Утилиты и ручное управление кэшем

import { store } from './store';
import { postsApi } from './services/api';

// Ручной запрос без компонента
const result = await store.dispatch(
  postsApi.endpoints.getPosts.initiate()
);

// Ручное обновление кэша
store.dispatch(
  postsApi.util.updateQueryData('getPosts', undefined, (draft) => {
    draft.push({ id: 999, title: 'Новый пост', body: '', userId: 1 });
  })
);

// Ручная инвалидация тегов
store.dispatch(postsApi.util.invalidateTags(['Post']));

// Сброс всего кэша API
store.dispatch(postsApi.util.resetApiState());

// Предзагрузка данных
store.dispatch(
  postsApi.util.prefetch('getPosts', undefined, { force: false })
);

Предзагрузка в компонентах

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

function PostsPreloader() {
  const prefetchPosts = useGetPostsQuery.usePrefetch
    ? postsApi.usePrefetchPost 
    : undefined;
    
  // Или используйте хук предзагрузки
  return (
    <div 
      onMouseEnter={() => {
        // Предзагружаем при наведении
        store.dispatch(
          postsApi.util.prefetch('getPostById', 1, { ifOlderThan: 60 })
        );
      }}
    >
      Наведите для предзагрузки
    </div>
  );
}

RTK Query vs React Query

Оба инструмента решают схожие задачи, но имеют разный подход:

КритерийRTK QueryReact Query
ИнтеграцияВстроен в Redux ToolkitНезависимая библиотека
ХранилищеRedux storeСобственный кэш
Bundle size~9KB (если Redux уже есть)~13KB
КонфигурацияЦентрализованная (createApi)Распределённая (useQuery)
DevToolsRedux DevToolsReact Query DevTools
МутацииАвтоматическая инвалидация теговРучная инвалидация
Infinite queriesПоддерживается (с serializeQueryArgs)Встроенная поддержка (useInfiniteQuery)
Оптимистичные обновленияonQueryStartedonMutate

Когда выбрать RTK Query

  • Проект уже использует Redux
  • Нужна централизованная конфигурация API
  • Важна типобезопасность в стиле Redux
  • Нужна глубокая интеграция с Redux state

Когда выбрать React Query

  • Нет Redux в проекте
  • Нужна более гибкая конфигурация отдельных запросов
  • Активно используется бесконечная прокрутка
  • Важен меньший размер бандла при отсутствии Redux

Лучшие практики

1. Организация API файлов

src/
├── store/
│   ├── index.ts          # Конфигурация store
│   └── hooks.ts          # Типизированные хуки
└── services/
    ├── baseApi.ts        # Базовый API (fetchBaseQuery)
    ├── postsApi.ts       # Эндпоинты постов
    └── usersApi.ts       # Эндпоинты пользователей

2. Обработка ошибок

import { FetchBaseQueryError } from '@reduxjs/toolkit/query';
import { SerializedError } from '@reduxjs/toolkit';

function isApiError(error: unknown): error is FetchBaseQueryError {
  return typeof error === 'object' && error !== null && 'status' in error;
}

function getErrorMessage(error: FetchBaseQueryError | SerializedError): string {
  if (isApiError(error)) {
    if (typeof error.status === 'number') {
      return `HTTP ${error.status}: ${JSON.stringify(error.data)}`;
    }
    return error.error;
  }
  return error.message ?? 'Неизвестная ошибка';
}

function PostsList() {
  const { data, error, isError } = useGetPostsQuery();

  if (isError) {
    return <div className="error">{getErrorMessage(error)}</div>;
  }

  return <ul>{data?.map(/* ... */)}</ul>;
}

3. Кастомные базовые хуки

// Хук с обработкой ошибок по умолчанию
function usePostsWithToast() {
  const result = useGetPostsQuery();
  
  useEffect(() => {
    if (result.isError) {
      toast.error('Не удалось загрузить посты');
    }
  }, [result.isError]);
  
  return result;
}

4. Нормализация данных

Для больших наборов данных используйте createEntityAdapter совместно с RTK Query:

import { createEntityAdapter } from '@reduxjs/toolkit';

const postsAdapter = createEntityAdapter<Post>();
const initialState = postsAdapter.getInitialState();

const api = createApi({
  endpoints: (builder) => ({
    getPosts: builder.query<ReturnType<typeof postsAdapter.setAll>, void>({
      query: () => 'posts',
      transformResponse: (response: Post[]) =>
        postsAdapter.setAll(initialState, response),
    }),
  }),
});

// Получение нормализованных данных через selectors
const selectPostsResult = api.endpoints.getPosts.select();
const selectPostsData = createSelector(
  selectPostsResult,
  (result) => result.data ?? initialState
);

export const { selectAll: selectAllPosts, selectById: selectPostById } =
  postsAdapter.getSelectors(
    (state: RootState) => selectPostsData(state)
  );

Заключение

RTK Query — мощный инструмент, который автоматизирует большинство задач при работе с API в React-приложениях. Благодаря встроенному кэшированию, автоматической инвалидации, поддержке TypeScript и интеграции с Redux DevTools, он значительно упрощает разработку и отладку.

Основные преимущества:

  • Меньше шаблонного кода: автоматическая генерация хуков
  • Умное кэширование: автоматическое управление состоянием данных
  • TypeScript-first: полная типизация из коробки
  • Оптимистичные обновления: улучшение UX без сложного кода
  • Интеграция с Redux: единое хранилище для всего состояния приложения
Стрелочка влевоSnapshots тестированиеRedux Toolkit - современный ReduxСтрелочка вправо

Постройте личный план изучения React до уровня Middle — бесплатно!

React — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по React

React тестирование с Vitest
Zustand: продвинутые паттерны, persist middleware и slicesUncontrolled Components: когда DOM управляет даннымиБезопасность в React: защита от XSS, CSRF и утечек данныхRender Props: гибкое управление рендерингом в ReactРефакторинг React-кода: техники и лучшие практикиReact с WebSocket — реалтайм в компонентахReact Native New Architecture: Fabric и JSIReact Native приложение с нуля до публикации в сторахПрофилирование React: как найти и устранить узкие местаЧастичное применение: как создавать компоненты без лишнего кодаИменование компонентов в React: соглашения и лучшие практикиЛенивая загрузка: как ускорить React-приложение в разыОшибки гидратации в React и способы их устраненияHOC в React: мастерство композиции компонентовАрхитектура больших React приложений: Feature-Sliced DesignuseMemo: как спасти производительность от тяжелых вычисленийError Boundaries: создаем надежные React-приложенияКонтролируемые компоненты в React: полный контроль над формамиCompound Components в React: создаем гибкие компоненты с мощным APIДокументирование компонентов в React: Storybook, JSDoc и READMEКомпозиция компонентов в React: строим гибкие интерфейсыКомментирование кода в React: когда и как писать комментарииCode Splitting в React: как уменьшить бандл и ускорить загрузку приложенияАсинхронные компоненты в React: новый стандарт работы с даннымиДоступность (a11y) в React: ARIA, семантика и клавиатурная навигация
Zustand — управление состоянием в ReactYup - валидация схемXState - конечные автоматыТемизация в ReactТестирование хуковTailwind CSS с ReactZod - валидация с TypeScriptSWR - библиотека для запросовStorybook - документация компонентовStyled Components — стилизация через JSSnapshots тестированиеRTK Query - работа с APIRedux Toolkit - современный ReduxRecoil — библиотека управления состоянием от FacebookВиртуализация списков с react-window: как отображать тысячи элементов без лаговReact Toastify - уведомления в ReactReact Testing LibraryСоздание таблиц в React гайд по react-tableReact Spring - анимацииРабота с формами и селектами в ReactReact Query (TanStack Query) - работа с серверомПлагины в React что это и как их использоватьReact PDF - работа с PDF файламиОбзор популярных библиотек для ReactReact Icons - библиотека иконок для ReactReact Hook Form — валидация форм в ReactReact Dropzone — загрузка файловПодключение Bootstrap к React-приложениюReact Beautiful DnD - перетаскивание элементовАнимация при монтировании компонентов в ReactМокирование APIMobX — реактивное управление состоянием в ReactМикрофронтенды с React (micro-frontends)Загрузка и индикаторыАнимация списков в ReactJotai - атомарное состояниеБесконечная прокруткаFramer Motion - библиотека анимацийEmotion — библиотека CSS-in-JSДинамические стили в ReactE2E тестирование с CypressCSSTransition - переходыCSS-in-JS — плюсы и минусыКонтекст vs Redux — когда что использоватьИспользование Chart.js в ReactAxios с ReactТестирование асинхронных компонентовОбработка ошибок API
Valtio: атомарный state management в ReactuseState в React что это и как использоватьuseSyncExternalStore — работа с внешними сторамиuseTransition - плавные переходы между состояниямиuseRef в React — создание ссылок на DOM и значенияuseOptimistic — оптимистичные обновления UIuseLayoutEffect в React — эффект до отрисовкиuseInsertionEffect — внедрение стилей до мутаций DOMuseImperativeHandle в React — настройка ref дочернего компонентаuseId — генерация уникальных идентификаторовuseFormStatus - отслеживание статуса отправки формыuseDeferredValue — отложенное обновление состоянияuseDebugValue — отладка кастомных хуковuseCallback в React — мемоизация функцийuseMemo в React: как и когда оптимизировать тяжелые вычисленияuseReducer — альтернатива useState для сложной логикиХук use() в React 19useEffect в React что это и как использоватьuseContext — работа с контекстом в ReactuseCallback в React — мемоизация функций и оптимизация ре-рендеровuseActionState в React 19React Stale Closures — устаревшие замыкания в хукахОптимизация рендеринга в React: от теории к глубокой практикеЧто такое useRef и как его применять в ReactКак и зачем использовать React HooksУправление состоянием в React через ContextКак предотвратить лишние ре-рендеры в React: полное руководствоuseMemo vs useCallback: подробное руководство по мемоизации в ReactНовые возможности React 19Правила хуков — правила использованияflushSync — синхронные обновления состояния в ReactuseEffect vs useLayoutEffect: в чём разница и какой хук выбрать?Кастомные хуки в React — создание собственных хуковuseState продвинутое использование в React
Transition API — плавные обновления интерфейса в ReactReact Suspense — приостановка рендераStrictMode в React — как находить ошибки на этапе разработкиСерверные компоненты React (RSC) — подробный разбор и практикаКак работает рендеринг в ReactЧто такое props в React и как их правильно использоватьКак работает JSX связка React и HTMLЧто такое React.js и как его использоватьКак использовать элементы в ReactКак использовать React DOM в проектеЧто такое компоненты в React и как их применятьРабота с children в ReactПорталы в React: рендер компонентов вне иерархии DOMFragment в React: группировка элементов без лишних узлов DOMCSS Modules в ReactConcurrent Mode — конкурентный режим в React
Открыть базу знаний

Лучшие курсы по теме

изображение курса

React и Redux Toolkit

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий