Все мы не любим писать много однообразного кода и хотим писать больше интересного кода) Давайте рассмотрим очень удобный инструмент для работы с API - RTK Query. Redux является популярной библиотекой управления состоянием, используемой во многих современных веб-приложениях. Однако, при работе с ней, может возникнуть проблема громоздкости и шаблонного кода. В этом случае, RTK query приходит на помощь, позволяя сократить объем необходимого кода при работе с Redux.
Знакомство и пример кода
К примеру, рассмотрим запрос к API при помощи Redux и при помощи RTK query. Сначала, приведем код, используемый для запроса к API при помощи Redux на примере получения и отображения списка постов:
// actions.js
// определение экшенов
export const fetchPostsRequest = () => ({
type: 'FETCH_POSTS_REQUEST',
});
export const fetchPostsSuccess = (posts) => ({
type: 'FETCH_POSTS_SUCCESS',
payload: posts,
});
export const fetchPostsFailure = (error) => ({
type: 'FETCH_POSTS_FAILURE',
payload: error,
});
// reducer.js
// начальное состояние Redux-стейта
const initialState = {
posts: [],
isLoading: false,
error: null,
};
// определение редюсеров
export default function postsReducer(state = initialState, action) {
switch (action.type) {
case 'FETCH_POSTS_REQUEST':
// устанавливаем isLoading в true, пока данные загружаются
return { ...state, isLoading: true };
case 'FETCH_POSTS_SUCCESS':
// устанавливаем isLoading в false и записываем полученные данные в стейт
return { ...state, isLoading: false, posts: action.payload };
case 'FETCH_POSTS_FAILURE':
// устанавливаем isLoading в false и записываем сообщение об ошибке в стейт
return { ...state, isLoading: false, error: action.payload };
default:
return state;
}
}
// thunk.js
// определение thunk-функции
import { fetchPostsRequest, fetchPostsSuccess, fetchPostsFailure } from './actions';
export const fetchPosts = () => async (dispatch) => {
dispatch(fetchPostsRequest());
try {
const response = await fetch('/api/posts');
const data = await response.json();
dispatch(fetchPostsSuccess(data));
} catch (error) {
dispatch(fetchPostsFailure(error.message));
}
};
//selectors.js
// определение селектора
export const postsSelector = state => state.posts
// component.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchPosts } from './thunk';
import { postsSelector } from './selectors.js'
export default function Posts() {
const dispatch = useDispatch();
const posts = useSelector(postsSelector);
const isLoading = useSelector((state) => state.isLoading);
const error = useSelector((state) => state.error);
useEffect(() => {
// вызываем thunk-функцию при монтировании компонента
dispatch(fetchPosts());
}, [dispatch]);
if (isLoading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error}</div>;
}
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
Теперь рассмотрим, как можно упростить этот код при помощи RTK query:
// service.js
// определение сервиса с использованием RTK Query
import { createApi } from '@reduxjs/toolkit/query';
export const postsApi = createApi({
reducerPath: 'postsApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
}),
}),
});
export const { useGetPostsQuery } = postsApi;
// component.js
import React, { useEffect } from 'react';
import { useGetPostsQuery } from './service';
export default function Posts() {
const { data: posts, isLoading, isError, error } = useGetPostsQuery();
if (isLoading) {
return <div>Loading...</div>;
}
if (isError) {
return <div>Error: {error.data}</div>;
}
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
Код без RTK Query содержит определение экшенов, редюсеров, thunk-функции, селектора и компонента, использующих Redux для запроса к API и работы с полученными данными.
Второй фрагмент кода содержит определение сервиса с использованием RTK Query и компонента, использующего useGetPostsQuery
для получения списка постов.
Обратите внимание, что второй фрагмент кода гораздо более короткий и не содержит множества шаблонного кода. RTK Query позволяет автоматически обрабатывать ошибки и кэшировать полученные данные.
Кэшеирование и тэги
А теперь давайте в наш пример с RTK Query добавим отправку поста:
// service.js
import { createApi } from '@reduxjs/toolkit/query';
export const postsApi = createApi({
tagTypes: ['Posts',],
reducerPath: 'postsApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
providesTags: ['Post'],
}),
addPost: builder.mutation({
query: (body) => ({
url: '/posts',
method: 'POST',
body,
}), // Добавление поста
invalidatesTags: ['Posts'],
}),
}),
});
export const { useGetPostsQuery, useAddPostMutation } = postsApi;
// component.js
import React, { useEffect, useState } from 'react';
import { useGetPostsQuery, useAddPostMutation } from './service';
export default function Posts() {
const { data: posts, isLoading, isError, error } = useGetPostsQuery();
const [title, setTitle] = useState('');
const [body, setBody] = useState('');
const [addPost, { isError: addError }] = useAddPostMutation();
const handleAddPost = async () => {
await addPost({ title, body });
setTitle('');
setBody('');
};
if (isLoading) {
return <div>Loading...</div>;
}
if (isError || addError) {
return <div>Error: {error?.data || addError?.data}</div>;
}
return (
<div>
<form onSubmit={handleAddPost}>
<input type="text" placeholder="Title" value={title} onChange={(e) => setTitle(e.target.value)} />
<textarea placeholder="Body" value={body} onChange={(e) => setBody(e.target.value)} />
<button type="submit">Add Post</button>
</form>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
После того как мы отправили на сервер новый пост нам нужно обновить список. Здесь нам помогут тэги. Теги в RTK query используются для работы с кэшем. В примере выше, мы используем providesTags
и invalidatesTags
в методах query
и mutation
, чтобы сообщить RTK query, какие теги должны быть связаны с данными, получаемыми или изменяемыми этими методами.
Теги, определенные в методе providesTags
, будут связаны со списком постов, полученных методом getPosts
. Теги, определенные в методе invalidatesTags
, будут инвалидированы при добавлении нового поста методом addPost
, чтобы обновить список постов, когда новый пост будет добавлен.
Также можно самим в нужных местах инвалидировать список:
<Button onClick={()=>postsApi.invalidateQueries("Post")}>
Update
</Button>
transformResponse
transformResponse
- это опция, которая позволяет изменять данные ответа, получаемые из сети, перед тем как они будут обработаны редьюсером. Она может быть полезна в случаях, когда необходимо произвести некоторые манипуляции с даннымит.
В данном случае, можно использовать опцию transformResponse
для того, чтобы привести данные ответа к нужному формату. Например, добавить поле date
для каждого элемента списка:
// service.js
// определение сервиса с использованием RTK Query
import { createApi } from '@reduxjs/toolkit/query';
export const postsApi = createApi({
reducerPath: 'postsApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
transformResponse: (response) => {
// приводим данные ответа к нужному формату
const posts = response.map((post) => ({
...post,
date: new Date(),
}));
return posts;
},
}),
}),
});
export const { useGetPostsQuery } = postsApi;
// component.js
import React, { useEffect } from 'react';
import { useGetPostsQuery } from './service';
export default function Posts() {
const { data: posts, isLoading, isError, error } = useGetPostsQuery();
...
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
{post.title} - {post.date.toLocaleString()}
</li>
))}
</ul>
);
}
queryFn
queryFn
позволяет использовать собственную функцию для выполнения запросов. Это может быть полезно, если вам нужно выполнить запросы в определенном порядке или в случае, если вам нужно обработать данные перед возвратом.
Например, представьте, что вам нужно выполнить два запроса на сервер: один для получения постов, а другой для получения авторов этих постов. Вы можете использовать queryFn
для выполнения обоих запросов и преобразования данных.
// service.js
import { createApi } from '@reduxjs/toolkit/query';
export const blogApi = createApi({
reducerPath: 'blogApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getBlogData: builder.query({
queryFn: async () => {
const [posts, authors] = await Promise.all([
fetch('/api/posts').then((res) => res.json()),
fetch('/api/authors').then((res) => res.json()),
]);
return { posts, authors };
},
transformResponse: (response) => {
const { posts, authors } = response;
const authorsById = {};
authors.forEach((author) => {
authorsById[author.id] = author;
});
const postsWithAuthors = posts.map((post) => ({
...post,
author: authorsById[post.authorId],
}));
return postsWithAuthors;
},
}),
}),
});
export const { useGetBlogDataQuery } = blogApi;
// component.js
import React from 'react';
import { useGetBlogDataQuery } from './service';
export default function Blog() {
const { data: blogData, isLoading, isError, error } = useGetBlogDataQuery();
...
return (
<ul>
{blogData.map((post) => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
<p>
Written by {post.author.name} ({post.author.email})
</p>
</li>
))}
</ul>
);
}
Ну или можно оставить получение постов отдельно и добавить еще эндпоинт получения авторов и постов с авторами:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query';
export const postsApi = createApi({
reducerPath: 'postsApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
}),
getAuthors: builder.query({
query: () => '/authors',
}),
}),
});
export const { useGetPostsQuery, useGetAuthorsQuery } = postsApi;
const getPostsWithAuthors = async () => {
const [posts, authors] = await Promise.all([
postsApi.endpoints.getPosts(),
postsApi.endpoints.getAuthors(),
]);
return posts.map((post) => ({
...post,
author: authors.find((author) => author.id === post.authorId),
}));
};
const customQueryFn = async (args, api, extraOptions) => {
const { data } = await getPostsWithAuthors();
return { data };
};
export const { useGetPostsWithAuthorsQuery } = postsApi.injectEndpoints({
endpoints: (builder) => ({
getPostsWithAuthors: builder.query({
queryFn: customQueryFn,
}),
}),
});
function PostsWithAuthors() {
const { data: posts, isLoading, isError, error } = useGetPostsWithAuthorsQuery();
...
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
{post.title} - {post.author.name}
</li>
))}
</ul>
);
}
Использование с createSelector
Когда вы используете createApi
из RTK Query, он автоматически генерирует селекторы для вас для каждого эндпоинта. Эти селекторы используются для выбора данных из стейта, которые были сохранены в результате выполнения запросов.
Однако, иногда вам может потребоваться создать собственный селектор, основанный на более сложной логике или на нескольких селекторах. Для этого можно использовать функцию createSelector
из библиотеки reselect
.
Пример использования createSelector
с RTK Query:
import { createSelector } from 'reselect';
import { useGetPostsQuery } from './service';
const selectPosts = (state) => state.postsApi.posts;
const selectAuthors = (state) => state.postsApi.authors;
const selectPostsAndAuthors = createSelector(
selectPosts,
selectAuthors,
(posts, authors) => {
return posts.map((post) => {
return {
...post,
author: authors.find((author) => author.id === post.authorId),
};
});
}
);
export default function Posts() {
const {
isLoading: isLoadingPosts,
isError: isErrorPosts,
} = useGetPostsQuery();
const {
isLoading: isLoadingAuthors,
isError: isErrorAuthors,
} = useGetAutorsQuery();
const postsAndAuthors = useSelector(selectPostsAndAuthors);
if (isLoadingPosts||isLoadingAuthors) {
return <div>Loading...</div>;
}
if (isErrorPosts||isErrorAuthors) {
return <div>Error</div>;
}
return (
<ul>
{postsAndAuthors.map((post) => (
<li key={post.id}>
<div>{post.title}</div>
<div>Author: {post.author.name}</div>
</li>
))}
</ul>
);
}
В этом примере мы создаем два базовых селектора selectPosts
и selectAuthors
, которые выбирают данные posts
и authors
из стейта. Затем мы создаем селектор selectPostsAndAuthors
с помощью функции createSelector
. Он принимает selectPosts
и selectAuthors
, а затем использует их для создания нового массива данных, который включает авторов для каждого поста.
Затем мы используем useSelector
для получения данных postsAndAuthors
из стейта. Далее мы используем эти данные в JSX для отображения списка постов с именами авторов.
Таким образом, использование createSelector
позволяет создавать более сложные селекторы на основе нескольких базовых селекторов, что может быть полезно для работы с данными в RTK Query.
Заключение
В заключении не могу не упомянуть похожую библиотеку React Query. которая также предоставляет инструменты для управления состоянием данных в React-приложении и упрощает работу с запросами к серверу. Она также предоставляет кеширование данных и автоматическую обработку ошибок, аналогично RTK Query.
Однако, есть несколько различий между RTK Query и React Query. Например, RTK Query использует Redux под капотом, в то время как React Query не зависит от него и может работать как с Redux, так и без него. Также RTK Query предоставляет дополнительные инструменты для работы с запросами, такие как механизм инвалидации данных, возможность автоматически сгенерировать хуки запросов на основе API-схемы и другие.
В целом, RTK Query является мощным инструментом для работы с запросами к API в Redux-приложениях. Она значительно упрощает разработку и поддержку кода и сокращает количество необходимого написания кода. Если вы работаете с Redux, то RTK Query стоит рассмотреть в качестве готового решения для работы с API.
Комментарии (6)
markelov69
22.04.2023 12:50+1Все мы не любим писать много однообразного кода и хотим писать больше интересного кода)
Вы лицемерите. Т.к. если бы это было правдой, и вы правда не хотели бы писать лишний и бесполезный код, ты вы би никогда не брали Redux/RTK/и т.д и т.п., а брали бы без альтернативно MobX в пару к реакту.
leonidshishkin Автор
22.04.2023 12:50Большие проекты обычно делают командой и на большинстве проектов стэйт-менеджер redux. В этой статье обзор того как в рамках именно redux уменьшить количество лишнего кода.
markelov69
22.04.2023 12:50на большинстве проектов стэйт-менеджер redux
Это отмазка и оправдание вашему говно-коду который вы тут предлагаете что-ли? Если на вашем проекте используется redux, то у меня для вас плохие новости, вы всё ещё в каменном веке. Более того, вы просто упускаете время, вместо того чтобы работать на нормальных проектах и с нормальными технологиями.
azizoid
22.04.2023 12:50Работаю в Берлине вот уже три года. И на разных интервью обязательно спрашивают про стейт-менеджмент. Я обязательно упоминаю про MobX, и предупреждаю, что он популярен в русскоязычном сегменте. Вы не поверите - пока не встречал никого кто бы его использовал, многие говорят, что и не слышали о нем. Я хочу перейти на него, но ссыкотно :/
markelov69
22.04.2023 12:50Вы не поверите - пока не встречал никого кто бы его использовал, многие говорят, что и не слышали о нем
О чем это говорит по вашему? Представьте каким нужно быть "специалистом" который работает годами с реактом и не слышал даже о MobX. Реакт не использовать с MobX'ом это уже лютая дичь, а если ещё о нем и не слышать в принципе это вообще смешно. Если по кайфу и не западло не выходить за пределы 2015года и писать говнокод, то вам не нужен MobX, можете писать на react+redux стандартный говнокод и быть типичным "специалистом" которому не в лом морать руки на таких проектах.
vitalus
Вот же вермишель. Для Hello World сгодится..