Все мы не любим писать много однообразного кода и хотим писать больше интересного кода) Давайте рассмотрим очень удобный инструмент для работы с 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)


  1. vitalus
    22.04.2023 12:50
    +1

    Вот же вермишель. Для Hello World сгодится..


  1. markelov69
    22.04.2023 12:50
    +1

    Все мы не любим писать много однообразного кода и хотим писать больше интересного кода)

    Вы лицемерите. Т.к. если бы это было правдой, и вы правда не хотели бы писать лишний и бесполезный код, ты вы би никогда не брали Redux/RTK/и т.д и т.п., а брали бы без альтернативно MobX в пару к реакту.


    1. leonidshishkin Автор
      22.04.2023 12:50

      Большие проекты обычно делают командой и на большинстве проектов стэйт-менеджер redux. В этой статье обзор того как в рамках именно redux уменьшить количество лишнего кода.


      1. markelov69
        22.04.2023 12:50

        на большинстве проектов стэйт-менеджер redux

        Это отмазка и оправдание вашему говно-коду который вы тут предлагаете что-ли? Если на вашем проекте используется redux, то у меня для вас плохие новости, вы всё ещё в каменном веке. Более того, вы просто упускаете время, вместо того чтобы работать на нормальных проектах и с нормальными технологиями.


        1. azizoid
          22.04.2023 12:50

          Работаю в Берлине вот уже три года. И на разных интервью обязательно спрашивают про стейт-менеджмент. Я обязательно упоминаю про MobX, и предупреждаю, что он популярен в русскоязычном сегменте. Вы не поверите - пока не встречал никого кто бы его использовал, многие говорят, что и не слышали о нем. Я хочу перейти на него, но ссыкотно :/


          1. markelov69
            22.04.2023 12:50

             Вы не поверите - пока не встречал никого кто бы его использовал, многие говорят, что и не слышали о нем

            О чем это говорит по вашему? Представьте каким нужно быть "специалистом" который работает годами с реактом и не слышал даже о MobX. Реакт не использовать с MobX'ом это уже лютая дичь, а если ещё о нем и не слышать в принципе это вообще смешно. Если по кайфу и не западло не выходить за пределы 2015года и писать говнокод, то вам не нужен MobX, можете писать на react+redux стандартный говнокод и быть типичным "специалистом" которому не в лом морать руки на таких проектах.