Перетяжка, Премия ТПрогер, 13.11
Перетяжка, Премия ТПрогер, 13.11
Перетяжка, Премия ТПрогер, 13.11

React и RTK Query — новый лёгкий путь для redux?

Разбираемся, как упростить запросы в react-redux с помощью redux toolkit. Пишем облегченные запросы с RTK query. Подойдет ли для всего?

760 открытий6К показов
React и RTK Query — новый лёгкий путь для redux?

Прежде всего, давайте вспомним, как выглядит классический запрос и хранение данные с redux-saga и @reduxjs/toolkit.

			// slice
// Используем enum со статусами чтобы легко манипулировать ими в дальнейшем
enum EOperationStatus {
  initial = "initial",
  pending = "pending",
  error = "error",
  success = "success",
}

const initialState: IBotInitialState = {
  items: [],
  itemsStatus: EOperationStatus.initial,
};

export const itemsSlice = createSlice({
  name: "items",
  initialState,
  reducers: {
    getItems: state => {
      state.itemsStatus = EOperationStatus.pending;
    },
    setItems: (state, {payload}: PayloadAction) => {
      state.items = payload;
    },
    setItemsStatus: (
      state, { payload}: PayloadAction,
    ) => {
      state.itemsStatus = payload;
    },
    ...

// api
const getItems = async () => await HttpHandler.get("/items");
export const API = {
  getItems, 
};

// saga
function* getItemsSaga() {
  try {
    const res = yield* call(API.getItems);
    yield* put(itemsSlice.setItems(res));
    // На простых запросах нам постоянно нужно будет писать одно и тоже:
    // сохраняем результат и обновляем статусы.
    yield* put(itemsSlice.setItemsStatus(EOperationStatus.success));
  } catch (e) {
    yield* put(itemsSlice.setItemsStatus(EOperationStatus.error));
  }
}

// selector
// Теперь нам понадобится 2 селектора
export const itemsSelector = selector(s => s.items);
export const itemsStatusSelector = selector(s => s.itemsStatus);

// Component.tsx
export const Component = () => {
  const items = useSelector(itemsSelector);
  const itemsStatus = useSelector(itemsStatusSelector);
  
  if(itemsStatus === EOperationStatus.initial || 
     itemsStatus === EOperationStatus.pending){
       return 'loading';
     }
     return <>{items.map(el => {el.id})}
}
		

Так выглядит стандартный шаблон запроса с манипулированием статусов и показом данных. Универсально, как швейцарский нож, и может быть полностью кастомизировано. На таком шаблоне мы вправе выполнять запросы любой сложности и контролировать все детали.

Но что если запросы такие же легкие как в примере выше? Мы просто берем данные и показываем их. Разве нам нужно каждый раз проделывать такую рутину, если можно облегчить себе жизнь?

Как раз для таких случаев есть инструмент rtk-query (входит в reduxjs/toolkit). Посмотрим как теперь выглядит наш запрос.

			// rtk
interface IItem {
  id: number;
}

const apiWithTags = rtkApi.enhanceEndpoints({});

export const itemsRtkApi = apiWithTags.injectEndpoints({
  endpoints: (build) => ({
    getItems: build.query<
    IItem[],
    undefined>({
      query: () => ({
        url: `/items?client=true`,
      }),
      async onQueryStarted(_, { queryFulfilled }) {
        try {
          await queryFulfilled;
        } catch (_e) {
          noop();
        }
      },
      keepUnusedDataFor: 0,
    }),
  }),
});

// Component
...
  const { data, isError, isFetching, isLoading, status,
   error, refetch } = itemsRtkApi.useGetItemsQuery(
    undefined,
    {refetchOnMountOrArgChange: true}
  );
...
		

В build.query мы передаем два аргумента, 1 — тип, который вернется и 2 — параметры, которые мы принимаем в хуке в компоненте.

Далее в компоненте мы вызываем хук и передаем данные для вызова апи и вторым аргументом, параметры для самого хука. Например, свойство refetchOnMountOrArgChange перезапросит данные при unmount компоненте. В документации можно подробнее прочитать о каждом из них.

Самое главное, что в ответе хука у нас появляются все статусы/данные/фичи. Каждый из них может быть полезен для конкретной ситуации, но все вместе они покрывает почти все кейсы.

			// Component.tsx
export const Component = () => {
  const { data: items, isError, isFetching } = itemsRtkApi.useGetItemsQuery(
    undefined,
    {refetchOnMountOrArgChange: true}
  );
  
  if(isFetching){
     return 'loading';
   }
  if(isError){
     return 'isError';
   }
   return <>{items.map(el => {el.id})}
}
		

Теперь компонент выглядит легко и изящно. Тут мы используем isFetching (отличие от isLoading в том, что он активируется при любой загрузке, а isLoading только при начальной). Можно вызывать refetch для перезапроса или проверить isError при ошибке.

Также мы можем вызывать хук с задержкой. Так называемый lazy query.

			// Component.tsx
export const Component = () => {
  const [fetchItems, 
  {isFetching, {isError, data: items, isUninitialized}] = itemsRtkApi.useLazyGetItemsQuery();
  
  useEffect(() => {
    fetchItems()
  }, [])
  
  if(isFetching || isUninitialized){
     return 'loading';
   }
  if(isError){
     return 'isError';
   }
   return <>{items.map(el => {el.id})}
}
		

Тут мы делаем запрос из хука. Обычно это нужно, если хотим передать аргумент в хук, который появляется не сразу. Также проверяем isUninitialized, чтоб хук был проинициализирован и далее при загрузке стал false.

Плюс к этому, rtk дает возможность использовать мутации для обновления данных. Это будет выглядеть примерно так:

			// rtk
...
updateItems: build.mutation({
  queryFn: async ({ param }, api, _extraOptions, baseQuery) => {
    const { data, error } = (await baseQuery(`/items/?param=${param}`)) as QueryReturnValue<
      IItem[],
      FetchBaseQueryError
    >;

    if (error) {
      return { error };
    }

    api.dispatch(
      itemsRtkApi.util.updateQueryData('getItems', undefined, (draft) => {
        return Object.assign(draft, data, [...(draft|| []), ...(data|| [])] });
      })
    );

    return { data };
  },
}),
...
		

Для чего не подходит rtk?

Для сложных операций, таких как пагинация или хранение переменных между запросами. В подобных кейсах лучше использовать саги и не ставить костыли с ртк. Ведь его главная цель избавить нас от рутинных запросов.

Следите за новыми постами
Следите за новыми постами по любимым темам
760 открытий6К показов