Топ-5 библиотек для управления состоянием React в 2025 году
- четверг, 27 февраля 2025 г. в 00:00:06
Хранение данных и управление глобальным состоянием в React-приложениях всегда было важной темой среди разработчиков. К 2025 году выбор подходящей библиотеки для решения этих задач стал еще более разнообразным — от проверенного Redux до современных, легковесных решений, таких как Zustand и SWR. Каждое из этих решений имеет свои особенности, плюсы и подводные камни, что делает выбор оптимального инструмента порой непростым. В этой статье я рассмотрю 5 самых популярных библиотек на сегодняшний день, проанализирую их основные преимущества, применение на реальных проектах и актуальность в контексте последних трендов разработки.
Привет, Хабр! Меня зовут Мария Кустова, я frontend-разработчик IBS. Подобного рода сравнительные исследования стейт-менеджеров выходят каждый год. Когда я начинала сбор информации, именно перевод похожей статьи стал для меня отправной точкой, но в ней были приведены другие библиотеки. Думаю, эта статья будет интересна тем, кто хочет узнать, что сейчас активно используют коллеги по React.
Сразу скажу про свой личный опыт: работа над текущим проектом по созданию гигантской системы-реестра ведется уже более 5 лет, и за это время команда перепробовала разные подходы к хранению данных. Ранее мы использовали React Redux в связке с Redux Saga, в некоторых местах применялся React Query. Кроме того, частично на проекте можно встретить использование Context от React. Сейчас мы поэтапно смещаемся в сторону RTK Query, особенно при создании новой функциональности.
Чтобы подготовить эту статью, я также реализовала несколько pet-проектов, в которых для организации хранения данных использовала актуальные на сегодняшний день стейт-менеджеры, с которыми мне не приходилось сталкиваться лично.
В качестве критерия популярности я выбрала показатель скачиваний пакета за неделю. Как видно, разработчики по-прежнему более охотно выбирают стейт-менеджер Redux, первая версия которого вышла десять лет назад — в 2015 году. Он опережает ближайшего конкурента по скачиваниям почти вдвое.
Redux — уже, можно сказать, классический инструмент для управления состоянием данных и пользовательским интерфейсом в приложениях. Название библиотеки составлено из двух слов: Reduce — функция, которая приводит большую структуру данных к одному значению, и Flux — архитектура приложения, при которой данные передаются в одну сторону. Redux имеет открытый исходный код и доступен бесплатно.
На втором месте по популярности находится Zustand — небольшое, быстрое и масштабируемое решение, которое базируется на принципах Flux и Immutable State (неизменное состояние). Zustand отличается удобным API, основанным на хуках, он не создает лишнего шаблонного кода (это был камушек в огород Redux) и не навязывает жестких правил использования.
Почетное третье место занимает RTK Query — мощный инструмент для получения и кеширования данных. Он предназначен для упрощения распространенных случаев загрузки данных в веб-приложении, избавляет от необходимости вручную писать логику загрузки и кеширования.
На четвертом месте — SWR — легкая библиотека от компании Vercel, подарившей нам Next.js. SWR позволяет получать, кешировать и ревалидировать данные в реальном времени с помощью React Hooks. Она построена с использованием React Suspense, который позволяет компонентам «ждать» получения данных, прежде чем они смогут отобразиться.
И завершает пятерку MobX — библиотека, которая дает разработчикам инструмент для глобального использования переменных и методов между разными компонентами.
Начну с лидера рейтинга. Поскольку Redux хорошо знаком примерно каждому разработчику React, я не буду разбирать его подробно, а только обозначу ключевые преимущества и недостатки этого решения.
Из хорошего:
Redux обеспечивает прозрачность и предсказуемость потока данных, так как все изменения состояния происходят только через actions и reducers — чистые функции, не зависящие от внешних факторов.
Redux позволяет масштабировать приложения, так как он предоставляет единый и стабильный интерфейс для управления состоянием, который не зависит от конкретных компонентов. Это облегчает разделение логики и представления, а также повторное использование и композицию компонентов.
Redux славится множеством вспомогательных библиотек и полезных инструментов, которые сильно облегчают работу с приложениями.
Из плохого:
Высокий порог вхождения и сложная кривая обучения.
Бойлерплейт: небольшое изменение функциональности может потребовать относительно больших изменений в коде, а добавление новой — однотипного и бесполезного для конкретной задачи кода.
Моностор: store является одной глобальной переменной. При каждом изменении какого-либо участка стора идет полное копирование ВСЕГО стора, что означает избыточные перерисовки и проблемы с производительностью. Правильное использование useSelector и библиотеки reselect помогают избавиться от лишних перерисовок.
Низкая связанность, высокое зацепление: чтобы степень связанности между системами оставалась низкой, необходимо распределять обязанности между объектами. В Redux то, что должно быть цельным внутри компонента, оказывается «размазанным» по множеству файлов и сущностей, а связи, которые должны оставаться внутри, выходят наружу.
А теперь перейдем к более молодым решениям, которые бросают вызов «старичку» Redux.
При анализе библиотек я заметила, что наиболее похожие подходы между собой имеют RTK Query и SWR, MobX и Zustand. Поэтому по определенным параметрам я буду сравнивать их попарно.
Размер пакета
Для работы с RTK Query необходимо установить два пакета общим весом в 6340 КВ, для работы с SWR — один пакет на 620 КВ, с Zustand — 88 KB. Для работы с MobX существует несколько вариантов пакетов:
mobx-react-lite на 408 КВ: работает с функциональными компонентами React;
mobx-react на 650 КВ: работает с функциональными и классовыми компонентами React.
Впрочем, на размер финального бандла в pet-проектах это не оказало существенного влияния.
Все перечисленные библиотеки поддерживают TS и не требуют скачивания отдельных пакетов для работы с типами.
Перейдем к принципу, на основе которого строится подход к хранению данных в каждой библиотеке:
Инструмент | Принцип |
RTK Query | В основе состояния лежит кеш. При первом запросе RTK Query отправляет запрос и сохраняет полученные данные в кеше. При последующих запросах к тому же эндпойнту RTK Query проверяет кеш на наличие сохраненных данных и, если они есть и время с последнего запроса не превышает 60 секунд, возвращает их, не делая нового запроса. |
SWR | Стратегия, которая сначала возвращает устаревшие данные из кеша, затем отправляет запрос на ревалидацию и в итоге возвращает актуальные данные. Важно отметить, что, если не передать соответствующие настройки, запросы будут «дергаться» регулярно. |
MobX | Основная идея MobX заключается в использовании наблюдаемых данных. Это позволяет библиотеке отслеживать изменения в этих данных и автоматически обновлять компоненты, которые зависят от них (шаблон проектирования Observer). |
Zustand | Использует однонаправленный подход Flux и паттерн «Издатель/подписчик». Это можно проиллюстрировать следующим порядком действий: действия пользователя в компоненте → изменение данных в хранилище → хук для отображения изменений. |
Теперь посмотрим, как создается хранилище в каждом из рассматриваемых стейт-менеджеров.
RTK Query
В отдельном файле мы создаем хранилище с помощью функции configureStore от @reduxjs/toolkit и передаем ему объект с настройками:
export const store = configureStore({
reducer: {
[rtkApi.reducerPath]: rtkApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(rtkApi.middleware),
});
Далее оборачиваем наше приложение в Provider от react-redux и передаем ему в качестве свойств созданное нами хранилище:
<Provider store={store}>
<App />
</Provider>
Данные хранятся в кэш.
Благодаря функции createApi мы можем создать и описать слайсы нашего хранилища, таким образом разделив его.
SWR
Не нужно специально создавать хранилище. Данные будут сохраняться под уникальным ключом, который необходимо передать первым аргументом в хук useSwr или useSwrMutation. По умолчанию SWR использует глобальный кеш для хранения и обмена данными между всеми компонентами. Но пользователь также может настроить это поведение с помощью опции provider в SWRConfig.
const {
data: post,
error,
isLoading,
} = useSWR(
${POSTS_QUERY_KEYS.POST_BY_ID}${id},
postsApi.getPostById,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
MobX
В отдельном файле мы создаем хранилище, которое может быть классом или функцией. Лучшей практикой считается использование классов. Основа создания наблюдаемых объектов — указание аннотации для каждого свойства с помощью функций makeObservable или makeAutoObservable.
export default class TodoStore {
todos: Todo[] = [];
userIds: number[] = [];
isLoading = false;
loadingId: number | null = null;
constructor() {
makeAutoObservable(this);
}
}
const todoStore = new TodoStore ();
Магия MobX начинается с того момента, когда мы передаем в качестве аргумента все хранилище в makeAutoObservable. «Под капотом» MobX анализирует переданный ему класс и помечает все свойства класса как наблюдаемые, все методы — как экшены, то есть функции, вызов которых приведет к изменению данных в хранилище, а все геттеры — как рассчитываемые свойства, которые пересчитываются при изменении данных в хранилище.
Чтобы компоненты стали наблюдаемыми и подхватывали все изменения, которые происходят в хранилище, необходимо обернуть каждый компонент в функцию observer. Здесь не нужно создавать контекст и передавать его с помощью провайдера, оборачивая в него все приложение. Также можно создать индивидуальное хранилище под каждую потребность.
// Создание хранилища с помощью класса
export default class TodoStore {
todos: Todo[] = [];
userIds: number[] = [];
isLoading = false;
loadingId: number | null = null;
constructor() {
makeAutoObservable(this);
}
loadTodos = async () => {
this.isLoading = true;
try {
const result = await todosApi.getTodos();
runInAction(() => {
if (result?.length) {
this.todos = result;
this.userIds = [...new Set(result.map(({ userId }) => userId))].sort(
(a, b) => b - a
);
}
this.isLoading = false;
});
} catch (err) {
console.log(err);
runInAction(() => {
this.isLoading = false;
});
}
};
...
}
// Создание общего хранилища
type StoreType = {
todosStore: TodosStore;
postsStore: PostsStore;
};
export const store: StoreType = {
todosStore: new TodosStore(),
postsStore: new PostsStore(),
};
// Извлечение данных из хранилища
const TodosList = observer(({ onEdit }: TodosListProps) => {
const { todosStore } = store;
const { editTodo, todos, loadingId, deleteTodo } = todosStore;
...
}
Zustand
Хранилище в Zustand — это хук. В нем можно хранить что угодно: примитивы, объекты или функции.
Мы можем создать хранилище с помощью функции create.
Функция set объединяет состояние:
const storeHook = create((set, get) => { ... store config ... });
Нет необходимости в провайдерах. Мы определяем состояние, и потребляющий компонент будет перерисовываться, когда это состояние изменится.
Также можно создавать индивидуальное хранилище под каждую потребность.
// Создание слайса общего хранилища
interface TodosStore {
todos: Todo[];
todosUserIds: number[];
isTodosLoading: boolean;
todosLoadingId: number | null;
}
interface TodosActions {
loadTodos: () => void;
addTodo: (todoText: string, userId: number) => void;
editTodo: (todo: Todo) => void;
deleteTodo: (id: number) => void;
}
export type TodosSlice = TodosStore & TodosActions;
export const createTodosSlice: StateCreator<TodosStore & TodosActions> = (
set
) => ({
todos: [],
todosUserIds: [],
isTodosLoading: false,
todosLoadingId: null,
loadTodos: async () => {
set({ isTodosLoading: true });
try {
const result = await todosApi.getTodos();
if (result) {
set({ todos: result });
set({ todosUserIds: result.map(({ userId }) => userId) });
}
set({ isTodosLoading: false });
} catch (err) {
console.log(err);
set({ isTodosLoading: false });
}
},
...
});
// Создание хука
export const useTodosStore = create<TodosSlice>()(
devtools(
(...props) => ({
...createTodosSlice(...props),
}),
{ enabled: true, name: "Todos", store: "Zustand Todos Store" }
)
);
Промежуточное программное обеспечение, или middleware, — это функции, имеющие доступ к объекту запроса, объекту ответа и к следующей функции промежуточной обработки в цикле «запрос — ответ». Другими словами, это функции, которые последовательно вызываются во время обновления данных в хранилище.
RTK Query
Пример подключения промежуточного программного обеспечения:
export const store = configureStore({
reducer: {
[rtkApi.reducerPath]: rtkApi.reducer,
},
/// Подключение ППО
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(rtkApi.middleware),
});
SWR
Промежуточное программное обеспечение (ППО) получает SWR-хук и может выполнять логику до и после его запуска.
function myMiddleware (useSWRNext) {
return (key, fetcher, config) => {
// До выполнения хука...
// Обработка следующего ППО, или хука useSWR, если это последнее.
const swr = useSWRNext(key, fetcher, config)
// После выполнения хука...
return swr
}
}
Если ППО несколько, каждый ППО оборачивает последующий. Последний ППО в списке получит исходный хук SWR — useSWR. Инструмент позволяет передать массив из нескольких ППО как опцию SWRConfig или useSWR.
<SWRConfig value={{ use: [myMiddleware] }}>
// или...
useSWR(key, fetcher, { use: [myMiddleware] })
Промежуточное ПО расширяется, как обычные опции.
function Bar () {
useSWR(key, fetcher, { use: [c] })
// ...
}
function Foo() {
return (
<SWRConfig value={{ use: [a] }}>
<SWRConfig value={{ use: [b] }}>
<Bar/>
</SWRConfig>
</SWRConfig>
)
}
Эквивалентно:
useSWR(key, fetcher, { use: [a, b, c] })
Zustand
В Zustand тоже можно создавать свое ППО. C ним поставляются готовые middlewares:
persist: для сохранения/восстановления данных из localStorage;
immer: для простого мутабельного изменения состояния;
devtools: для работы с расширением Redux DevTools для отладки.
/// Подключение devtools middleware
export const usePostsStore = create<PostsSlice>()(
devtools(
(...props) => ({
...createPostsSlice(...props),
}),
{ enabled: true, name: "Posts", store: "Zustand Posts Store" }
)
);
MobX
В стандартных пакетах MobX ППО, увы, не предусмотрены.
RTK Query
Взаимодействие с API задается с помощью эндпойнтов, которые определены в момент инициализации API. Мы начинаем с создания Api-slice с помощью функции createApi, в которую передаем следующие параметры:
reducerPath — уникальный ключ, который будет добавлен в хранилище;
baseQuery — параметр, который отвечает за непосредственное взаимодействие с API; в состав RTK Query входит инструмент под названием fetchBaseQuery, представляющий собой легковесную обертку над fetch, который подходит для большинства операций по работе с API;
endPoints — набор взаимодействий с API; существует два вида endpoints — query для получения данных и mutation для их изменения.
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
export const rtkApi = createApi({
reducerPath: "rtkApi",
tagTypes: ["Todos", "Posts"],
baseQuery: fetchBaseQuery({ baseUrl: "http://localhost:3001/" }),
endpoints: () => ({}),
});
Исходя из заданных эндпойнтов, RTK Query автоматически создает хуки. Эти хуки могут быть использованы непосредственно в React-компонентах для загрузки/отображения/изменения данных. Механизм взаимодействия с API инкапсулирован.
const BASE_URL = "/posts";
const postsApi = rtkApi.injectEndpoints({
endpoints: (build) => ({
getPosts: build.query<Post[], undefined>({
query: () => ({
url: BASE_URL,
}),
providesTags: ["Posts"],
}),
…
}),
overrideExisting: false,
});
export const {
useGetPostsQuery,
…
} = postsApi;
SWR
Так выглядит стандартный хук useSwr, который применяется прямо в компоненте:
// Пример из документации
const { data, error, isLoading, isValidating, mutate } = useSWR(key, fetcher, options)
В качестве параметров он принимает:
key — уникальный строчный ключ для запроса; может быть функцией, массивом, строкой или null;
fetcher — промис, возвращающий функцию для выборки;
options — объект опций для SWR-хука.
// Пример из pet-проекта
const {
data: todos = [],
error,
isLoading: isTodosLoading,
} = useSWR(TODOS_QUERY_KEYS.LOAD_TODOS, todosApi.getTodos, {
revalidateOnFocus: false,
});
В рамках pet-проекта я вынесла API в отдельный файл. Стоит обратить внимание, что уникальный ключ может использоваться в формировании эндпойнта (url = TODOS_QUERY_KEYS.LOAD_TODOS = ‘/todos’).
// Описание API
export const todosApi = {
getTodos: (url: string) =>
HttpClientBaseQuery<Todo[]>({ url }).then((response) => response.data),
…
};
MobX
В MobX создание API выносится в отдельный файл, а используется прямо в созданном хранилище.
const BASE_URL = "/posts";
export const postsApi = {
getPosts: () =>
HttpClientBaseQuery<Post[]>({ url: BASE_URL }).then(
(response) => response.data
),
…
};
// Пример получения списка постов:
export default class PostsStore {
post: Post | null = null;
posts: Post[] = [];
tagsList: Tag[] = [];
userIds: number[] = [];
loadingInitial = false;
loadingId: string | null = null;
constructor() {
makeAutoObservable(this);
}
loadPosts = async () => {
this.loadingInitial = true;
try {
const result = await postsApi.getPosts();
runInAction(() => {
if (result?.length) {
this.posts = result;
this.userIds = [...new Set(result?.map(({ userId }) => userId))];
}
this.loadingInitial = false;
});
} catch (err) {
console.log(err);
runInAction(() => {
this.loadingInitial = false;
});
}
};
...
};
В компоненте нам достаточно вызвать функцию loadPosts в useEffect, чтобы запустить асинхронный запрос на сервер и вызвать изменение наблюдаемых свойств.
Zustand
API в Zustand — это тоже отдельный файл. Очень похожая реализация с MobX. В хранилище описываются методы, которые отправляют запросы на сервер и изменяют значения данных хранилища. Вызов этих методов происходит в useEffect в самих компонентах.
const BASE_URL = "/posts";
export const postsApi = {
getPosts: () =>
HttpClientBaseQuery<Post[]>({ url: BASE_URL }).then(
(response) => response.data
),
...
};
// Описание метода (экшена) в хранилище:
export const createPostsSlice: StateCreator<PostsSlice> = (set) => ({
post: null,
posts: [],
tagsList: [],
postsUserIds: [],
postsLoadingInitial: false,
postsLoadingId: null,
loadPosts: async () => {
set({ postsLoadingInitial: true });
try {
const result = await postsApi.getPosts();
if (result?.length) {
set({ posts: result });
set({ postsUserIds: [...new Set(result.map(({ userId }) => userId))] });
}
set({ postsLoadingInitial: false });
} catch (err) {
console.log(err);
set({ postsLoadingInitial: false });
}
},
});
RTK Query
const { data } = api.endpoints.getPosts.useQuery();
const [updatePost, { data }] = api.endpoints.updatePost.useMutation();
const { data } = api.useGetPostsQuery();
const [updatePost, { data }] = api.useUpdatePostMutation();
Доступ к данным из кэша можно получить любым из перечисленных способов, то есть либо через эндпойнты, либо с помощью автоматически сгенерированных хуков.
Пример извлечения данных из автоматически сгенерированного хука:
const PostCard = () => {
const { id } = useParams();
const navigate = useNavigate();
const { data: post, error, isLoading } = useGetPostByIdQuery(id ?? skipToken);
...
}
Также мы можем извлечь из него дополнительную полезную информацию, такую как:
error — объект ошибки со всей необходимой информацией;
isUninitialized — флаг, который показывает, что запрос еще не был запущен;
isLoading — флаг, который показывает, был ли запущен первый запрос;
isFetching — флаг, который указывает на то, что первичный или повторный запрос запущен;
isError — флаг, который указывает, произошла ли ошибка во время запроса;
currentData — последний возвращенный результат;
isSuccess — флаг, который указывает на успешный запрос.
Кроме того, здесь же можно получить функцию принудительной повторной загрузки запроса — refetch.
SWR
Для получения данных в SWR есть несколько способов. Например, мы можем использовать хук useSWRConfig, чтобы получить доступ к текущему провайдеру кеша:
const { cache, ...extraConfig } = useSWRConfig()
Чтобы получить данные о конкретном запросе, нужно использовать метод get и ключ запроса:
const {data, error, isLoading} = cache.get(TODOS_KEY)
Чтобы записать данные в кеш, нужно использовать метод set и ключ запроса:
cache.set(TODOS_USER_IDS_KEY, {data: todos.map(({id}) => id)})
Пример из моего pet-проекта, напоминающий функциональность RTK Query. Данные извлекаются с помощью хука useSWR и описываются в том компоненте, которому они необходимы:
const {
data: todos = [],
error,
isLoading: isTodosLoading,
} = useSWR(TODOS_QUERY_KEYS.LOAD_TODOS, todosApi.getTodos, {
revalidateOnFocus: false,
});
Из хука можно получить дополнительную информацию о:
data — данные для ключа, разрешенные fetcher (или undefined, если не загружено);
error — ошибка, выброшенная fetcher-ом (или undefined);
isLoading — есть ли текущий запрос и нет ли «загруженных данных»; резервные данные и предыдущие данные не считаются «загруженными данными»;
isValidating — если запрос или ревалидация загружается;
mutate — функция для изменения закешированных данных.
MobX
Данные в MobX извлекаются прямо из хранилища через компонент TodoList, который отрисовывает список задач. Кроме того, извлекаются и экшены, вызывая которые мы можем удалять или редактировать данные в хранилище:
const TodosList = observer(({ onEdit }: TodosListProps) => {
/// Извлечение данных из хранилища
const { todosStore } = store;
const { editTodo, todos, loadingId, deleteTodo } = todosStore;
...
});
Здесь нет флагов, которые скажут нам, есть ли ошибка или идет ли загрузка. Весь этот набор данных необходимо будет предусмотрительно вычислять самостоятельно в хранилище. Впрочем, точно также, как это происходит и в обычном Redux:
export default class PostsStore {
post: Post | null = null;
posts: Post[] = [];
tagsList: Tag[] = [];
userIds: number[] = [];
loadingInitial = false;
loadingId: string | null = null;
constructor() {
makeAutoObservable(this);
}
loadPosts = async () => {
this.loadingInitial = true; // Ручное изменение флага состояния загрузки
try {
const result = await postsApi.getPosts();
runInAction(() => {
if (result?.length) {
this.posts = result;
this.userIds = [...new Set(result?.map(({ userId }) => userId))];
}
this.loadingInitial = false; // Ручное изменение флага состояния загрузки
});
} catch (err) {
console.log(err);
runInAction(() => {
this.loadingInitial = false; // Ручное изменение флага состояния загрузки
});
}
};
...
}
Zustand
Данные в Zustand извлекаются прямо из хуков. Сродни RTK Query, здесь можно передавать в качестве аргумента функцию selector, которая будет извлекать из хранилища ровно те данные, которые необходимы конкретному компоненту.
Как и в MobX, дополнительные данные придется вычислять самостоятельно.
const TodosList = ({ onEdit }: TodosListProps) => {
const { loadTodos, editTodo, todos, todosLoadingId, deleteTodo } =
useTodosStore();
...
}
Автоматическое обновление данных возможно только в двух библиотеках из списка — RTK Query и SWR. Под обновлением здесь понимается повторный вызов запроса при смене аргументов.
RTK Query
Обновление данных в RTK Query будет запущено автоматически, если изменится значение аргумента, который передается в хук. Также есть возможность настроить обновление данных при фокусе или переподключении к сети. Эти функции по умолчанию выключены, но их можно включить через опции refetchOnFocus и refetchOnReconnect.
Также есть возможность настройки запросов с помощью refetchOnMountOrArgChange. Позволяет принудительно перезагружать запрос при монтировании или если прошло свыше 60 секунд с момента последнего запроса для того же кеша.
RTK Query отличает возможность отказаться от кеширования данных или указать им новую величину времени, когда данные будут считаться валидными, с помощью опции keepUnusedDataFor.
SWR
Обновление данных происходит при изменении значения аргумента хука. Также осуществляется повторный запрос данных при фокусе или переподключении к сети, но, в отличие от RTK Query, в SWR эти функции по умолчанию включены. Их можно отключить через опции revalidateOnFocus и revalidateOnReconnect. Если мы знаем, что данные не изменяются от запроса к запросу, можно отменить повторные запросы с помощью опции revalidateIfStale: false.
В SWR можно задать интервал поллинга (errorRetryInterval) и максимальное количество запросов (errorRetryCount), а также делать принудительную перезагрузку запроса при монтировании (revalidateOnMount) и отправлять повторный запрос при ошибке (shouldRetryOnError).
RTK Query
Благодаря настройке skip, в RTK Query можно ограничить запрос по условию.
const [skip, setSkip] = useState(request.status !== RequestStatus.PROCESSING);
const { data } = statisticsSentFlApi.useUpdateStatSentFlRequestListItemQuery(request.requestId, {
skip,
pollingInterval: 5000,
});
Если ограничение нужно только на случай, когда аргумент может быть undefined, для условной выборки используется опция skipToken.
const { data: post, error, isLoading } = useGetPostByIdQuery(id ?? skipToken);
SWR
В SWR также доступна условная выборка, но реализована она несколько иначе. Для управления ревалидацией в SWR можно использовать null или передать функцию в качестве ключа для условной выборки данных. Если функция выводит ошибку или возвращает false, SWR не отправляет запрос.
const { data } = useSWR(shouldFetch ? '/api/data' : null, fetcher)
RTK Query — единственная из перечисленных библиотек, которая обладает полезным механизмом зависимых запросов. Для автоматизации повторной выборки RTK Query использует систему «тегов кеша» (providesTags/invalidatesTags). Это позволяет проектировать API таким образом, что запуск определенной мутации приведет к тому, что определенная конечная точка запроса будет считать свои кешированные данные недействительными и повторно извлечет данные при наличии активной подписки. Таким образом, мы можем не думать о последовательности запускаемых запросов.
const postsApi = rtkApi.injectEndpoints({
endpoints: (build) => ({
getPosts: build.query<Post[], undefined>({
query: () => ({
url: BASE_URL,
}),
providesTags: ["Posts"],
}),
createPost: build.mutation<
Post,
{ title: string; userId: number; tags: string[]; body: string }
>({
query: (post) => ({
url: ${BASE_URL},
method: "POST",
body: {
...post,
reactions: {
likes: 0,
dislikes: 0,
},
},
}),
invalidatesTags: ["Posts"],
}),
...
overrideExisting: false,
});
При отправке запроса на удаление поста следом запустится запрос на получение обновленного списка постов.
RTK Query
Здесь все довольно просто. В самом компоненте, который у нас будет, например, отрисовывать список постов, мы заводим локальный state, где будем хранить состояние индекса текущей страницы. Этот индекс мы будем передавать в качестве аргумента к useListPostsQuery. При изменении аргумента будет вызываться новый запрос и подгружаться новые данные. Пользователь, кликая и меняя этот индекс, будет таким образом вызывать запрос. Обращаю внимание, что если пользователь решит вернуться на предыдущую страницу, то запрос отправлен не будет, данные просто возьмутся из кеша.
const PostList = () => {
const [page, setPage] = useState(1)
const { data: posts, isLoading, isFetching } = useListPostsQuery(page)
if (isLoading) {
return <div>Loading</div>
}
if (!posts?.data) {
return <div>No posts :(</div>
}
return (
<div>
{posts.data.map(({ id, title, status }) => (
<div key={id}>
{title} - {status}
</div>
))}
<button onClick={() => setPage(page - 1)} isLoading={isFetching}>
Previous
</button>
<button onClick={() => setPage(page + 1)} isLoading={isFetching}>
Next
</button>
</div>
)
}
// Пример API
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (builder) => ({
listPosts: builder.query<ListResponse<Post>, number | void>({
query: (page = 1) => posts?page=${page},
}),
}),
})
SWR
Пагинация в SWR реализуется точно таким же образом, что и в RTK Query. Единственным здесь отличием является то, что при изменении индекса страницы каждый раз будет запускаться запрос.
function App () {
const [pageIndex, setPageIndex] = useState(0);
// URL-адрес API включает индекс страницы, который является состоянием React.
const { data } = useSWR(/api/data?page=${pageIndex}, fetcher);
// ... обработка состояния загрузки и ошибки
return <div>
{data.map(item => <div key={item.id}>{item.name}</div>)}
<button onClick={() => setPageIndex(pageIndex - 1)}>Назад</button>
<button onClick={() => setPageIndex(pageIndex + 1)}>Вперёд</button>
</div>
}
RTK Query
RTK Query предоставляет возможность реализации бесконечной загрузки. Ее суть заключается в сериализуемых аргументах и слиянии с предыдущими полученными данными:
createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
listItems: build.query<string[], number>({
query: (pageNumber) => /listItems?page=${pageNumber},
// Only have one cache entry because the arg always maps to one string
serializeQueryArgs: ({ endpointName }) => {
return endpointName
},
// Always merge incoming data to the cache entry
merge: (currentCache, newItems) => {
currentCache.push(...newItems)
},
// Refetch when the page arg changes
forceRefetch({ currentArg, previousArg }) {
return currentArg !== previousArg
},
}),
}),
})
SWR
В свою очередь, SWR предоставляет для реализации бесконечной загрузки специальный API — useSWRInfinite.
Пример:
const { data, error, isLoading, isValidating, mutate, size, setSize } = useSWRInfinite(
getKey, fetcher?, options?
)
Параметры:
getKey — функция, которая принимает индекс и данные предыдущей страницы и возвращает ключ страницы;
options — принимает все опции, которые поддерживает useSWR, с рядом дополнительных опций:
initialSize = 1: количество страниц, которые должны быть загружены изначально;
revalidateAll = false: всегда пытаться ревалидировать все страницы;
revalidateFirstPage = true: всегда пытаться ревалидировать первую страницу;
persistSize = false: не сбрасывать размер страницы до 1 (или initialSize, если установлен), когда ключ первой страницы изменяется;
parallel = false: загружать много страниц параллельно.
Возвращаемые значения:
data — массив значений ответа выборки каждой страницы;
mutate — то же, что и связанная функция мутации в useSWR, но манипулирует массивом данных;
size — количество страниц, которые будут извлекаться и возвращаться;
setSize — установить количество страниц, которые необходимо извлечь.
Подведем предварительные итоги:
Параметр | RTK Query | SWR |
Кеш по | Конечная точка + сериализованные аргументы | Определяемый пользователем ключ запроса |
Аннулирование кеша | Время или значение аргумента | Время или новые данные |
Опрос (поллинг) | Да | Да |
Потоковая передача (WebSocket) | Да | Да |
Зависимые запросы | Да | Нет |
Пропуск запроса | Да | Да |
Бесконечная загрузка | Да | Да |
Пагинация | Да | Да |
Предварительная выборка | Да | Да |
Повторная попытка | Да | Да |
Оптимистичные/пессимистичные обновления | Можно обновить кеш вручную | Можно обновить кеш вручную |
Ручная манипуляция кешем | Да | Да |
Исходные данные | Да | Да |
Трансформация ответа/ошибки | Да | Нет |
Параллельные запросы | Да | Да |
Реализация перечисленной выше функциональности в MobX и Zustand — в основном ручной труд разработчика.
RTK Query
const { data} = useGetPostsQuery()
Количество рендеров зависит только от изменения data. Если мы дополнительно будем использовать error, isFetching, isLoading, то количество рендеров будет зависеть от каждого параметра.
SWR
Встроенное кеширование и дедупликация SWR пропускают ненужные сетевые запросы. SWR обновляет только состояния, используемые компонентом.
const { data } = useSWR('/api', fetcher)
Количество рендеров будет зависеть от каждого извлекаемого параметра.
MobX
В MobX функция оbserver гарантирует, что компоненты не будут повторно отрисовываться, если нет соответствующих изменений в данных, которые используются в компоненте. «Под капотом» observer оборачивает компонент в React.memo.
Также в MobX реализован батчинг: если в коде предусмотрено несколько изменений наблюдаемых свойств, то перерендеринг случится один раз.
Computed-свойства (наблюдаемые свойства) кешируются. Если они не используются в компоненте, то они не будут и пересчитываться. Если наблюдаемые свойства изменились, но результат computed-свойств после вычислений остался прежним, то перерендеринга не будет.
Zustand
Компонент будет повторно рендериться только при изменении выбранного состояния. Чтобы избежать повторной визуализации там, где по факту не было изменений, рекомендуется использовать хук useShallow.
RTK Query
Расширение в Chrome Web Store — хорошо всем знакомая панель Redux DevTools. Большой плюс — возможность получить доступ ко всему хранилищу и посмотреть подробную информацию о каждом запросе.
SWR
Расширение в Chrome Web Store — SWR DevTools. В панели нет возможности посмотреть все хранилище, можно лишь отследить в левой части экрана экшены по уникальным ключам запроса.
MobX
Расширение в Chrome Web Store — MobX Developer Tools. Нет возможности посмотреть все хранилище, можно лишь отследить экшены. На мой взгляд, это самая неудобная панель разработчика из всего списка.
Zustand
Расширение в Chrome Web Store — Redux DevTools. Очень удобное представление хранилища. Плюс можно доработать, чтобы функции были не анонимными, а получили свое название.
Выбор библиотеки в первую очередь зависит от того, для какого приложения вы планируете организовать хранение данных. Если речь идет о крупном проекте, то лучше сразу выбирать между RTK Query и SWR. Обе библиотеки очень много делают за разработчика, и это приятно. Лично для меня настоящим открытием стала библиотека SWR — она составляет крепкую конкуренцию зарекомендовавшей себя RTK Query. Если же вы работаете над небольшим React-приложением, то Zustand или MobX должны без проблем закрыть все базовые потребности.