Решаем проблемы REST с помощью Redux Toolkit Query

от автора

В приложениях с REST архитектурой существует ряд проблем:

  • повторяющийся код при работе с состоянием приложения;

  • костыли и велосипеды при обработке результатов и состояний запросов;

  • отсутствие стандартного механизма кеширования полученных на клиенте данных;

  • одновременные запросы за одними и теми же данными; 

  • сложности реализации pessimistic/optimistic обновления состояний.

В клаудных микросервисах Netcracker мы решаем эти проблемы с помощью GraphQl & apollo. Однако есть изрядное количество приложений, использующих классический REST подход для общения с сервером. Хорошим решением для них является Redux Toolkit Query.

Netcracker стремится оптимизировать разработку клиентской части приложений на React. В начале пути мы использовали JavaScript + redux + axios для работы с состоянием приложения. В целом все было неплохо, вот только количество повторяющегося кода в redux зашкаливало, да и отсутствие типизации с болью отзывалось при любых UI изменениях. На помощь пришли Typescript и Redux-toolkit, украсив типизацией и слайсами наши front-end будни.

В крупных компаниях решение стандартных проблем с REST обычно отнимает большое количество времени и сил разработчиков. Настало время это исправить с помощью Redux toolkit query.

Документация Redux toolkit query хороша в теоретической части, но не покрывает некоторых особенностей, с которыми мы сталкиваемся на реальных проектах.

На самом redux и redux-toolkit останавливаться не будем. (про редакс, про redux toolkit)

Также к вашему вниманию:
Базовый пример стандартного CRA приложения с RTK Query.

Обратите внимание на минимальное количество кода, необходимое при работе с состоянием приложения.

Пример использования api
// Пример использования api export const exampleApi = commonApi.injectEndpoints({     endpoints: build => ({         fetchExampleList: build.query<ExampleModel[], number | void>({             query: (limit: number = 5) => ({                 url: '/example',                 params: {                     limit,                 },             }),             providesTags: result => [{ type: 'Example', id: 'List' }],         }),         createExample: build.mutation<ExampleModel, { example: Partial<ExampleModel> & { limit?: number } }>({             query: ({ example }) => ({                 url: '/example',                 method: 'POST',                 body: example,             }),             async onQueryStarted({ example }, { dispatch, queryFulfilled }) {                 try {                     const { data } = await queryFulfilled;                      dispatch(                         exampleApi.util.updateQueryData('fetchExampleList', example.limit, draft => {                             draft.unshift(data);                         })                     );                 } catch (e) {                     console.error('userApi createUser error', e);                 }             },         }),         updateExample: build.mutation<ExampleModel, { example: ExampleModel }>({             query: ({ example }) => ({                 url: `/example`,                 method: 'PUT',                 body: example,             }),             invalidatesTags: ['Example'],         }),         deleteExample: build.mutation<ExampleModel, { example: ExampleModel }>({             query: ({ example }) => ({                 url: `/example/${example.id}`,                 method: 'DELETE',             }),             invalidatesTags: ['Example'],         }),     }), }); 

Также на созданные с помощью RTK Query хуки, позволяющие стандартизовать обработку результатов и состояний запросов:

Пример автоматически сгенерированных хуков
const { data: examples = [], isLoading: examplesLoading } = exampleApi.useFetchExampleListQuery(); const [createExampleMutation, { isLoading: createExampleLoading }] = exampleApi.useCreateExampleMutation(); const [deleteExampleMutation, { isLoading: deleteExampleLoading }] = exampleApi.useDeleteExampleMutation(); const [updateExampleMutation, { isLoading: updateExampleLoading }] = exampleApi.useUpdateExampleMutation();

Приступим к рассмотрению неявных особенностей данной библиотеки:

1) Использование common.api.ts

Следует создать common.api.ts в самом начале. (Тут nota bene, на момент написания статьи в RTK (версии === 1.6.2) typescript не генерировал хуки в случае импорта createApi не из ‘@reduxjs/toolkit/dist/query/react’ и typescript версии < 4.1).

CommonApi – сущность, которая будет хранить общие настройки. Ее удобно расширять остальными *api в приложении, которые автоматически получат baseUrl (будет добавляться ко всем запросам), headers (см. пример) и tagTypes (для инвалидации кешей).

Пример создания commonApi
// src/store/common.api.ts export const commonApi = createApi({     reducerPath: 'api',     baseQuery: fetchBaseQuery({         baseUrl: BASE_URL,         prepareHeaders: headers => {             headers.set('Content-Type', 'application/json;charset=UTF-8');             headers.set('Authorization', 'anonymous');              return headers;         },     }),     tagTypes: ['Example'],     endpoints: _ => ({}), });   // src/store/store.ts const rootReducer = combineReducers({ …     [commonApi.reducerPath]: commonApi.reducer, … });  export const store = configureStore({     reducer: rootReducer,     middleware: getDefaultMiddleware => getDefaultMiddleware().concat(commonApi.middleware),     … }); 

2) Расширение commonApi чанками.

Каждый новый *api создаем, расширяя базовый commonApi, при этом больше не надо изменять store.ts, что очень удобно!

// src/store/example/example.api.ts export const exampleApi = commonApi.injectEndpoints({     endpoints: …

3) Pessimistic & Optimistic Updates

В интернете обычно представлены примеры запросов RTK query с последующим сбросом его кешей. Рассмотрим случай добавления/удаления сущности. После каждого подобного запроса RTK query отправит дополнительный гет запрос, чтобы получить самое последнее состояние. На практике же дополнительный запрос ни к чему.  В зависимости от вашего мировоззрения (шутка) следует использовать pessimistic/optimistic обновление данных в кеш. Это избавит вас от ненужных запросов. В основном используем pessimistic обновление, после ответа сервера.

Пример pessimistic обновления
createExample: build.mutation<ExampleModel, { example: Partial<ExampleModel> & { limit?: number } }>({     query: ({ example }) => ({         url: '/example',         method: 'POST',         body: example,     }),     async onQueryStarted({ example }, { dispatch, queryFulfilled }) {         try {             const { data } = await queryFulfilled;              dispatch(                 exampleApi.util.updateQueryData('fetchExampleList', example.limit, draft => {                     draft.unshift(data);                 })             );         } catch (e) {             console.error('exampleApi createExample error', e);         }     }, }), 

4) Разница между onQueryStarted и queryFn

Часто при работе с асинхронными вызовами, до и после отправки запроса, необходимо осуществить дополнительное действие. Для этих целей стоит использовать onQueryStarted. Модифицировать запрос не получится, однако возможно отследить его состояние с помощью queryFulfilled.

Пример onQueryStarted
fetchEntity: build.query<EntityModel, { id: string }>({     query: ({ id }) => ({         url: getEntityUrl(id),     }),     async onQueryStarted(arg, { dispatch, queryFulfilled }) {         try {             const result = await queryFulfilled;             dispatch(setEntityAction(result.data));         } catch (e) {             await const { unsubscribe } = dispatch(entityApi.endpoints.postEntityIdOnBE.initiate({ entityId: '' }));      unsubscribe();              console.error('fetchEntity error', e);         }     }, }), 

Если же требуется полностью контролировать запрос, добавить к нему хедеры, формировать тело запроса с использованием текущего состояния, сделать кастомный action (возможно вообще без запроса) – в этих случаях стоит использовать queryFn и встроенную обертку браузерного fetch – fetchWithBQ

Пример queryFn
deanonymizeCustomer: build.mutation<             CustomerModel,             { customer: CustomerInputModel }             >({             async queryFn({ customer }, { getState, dispatch }, extraOptions, fetchWithBQ) {                 const state = getState() as RootState;                 const customerId = state.customer?.id;                  if (!customerId) throw new Error('Deanonymize customer  error, no customerId');                  const body = getDeanonymizeCustomerData(customer);                  const result = await fetchWithBQ({                     url: getDeanonCustomerUrl(customerId),                     method: 'POST',                     body,                 });                  if (result.error) throw result.error;                  const data = result.data as CustomerModel;                  return { data };             },         }),

5) RTK query и его место в приложении

Стоит отметить, что RTK query не заменит работу с состоянием приложения полностью. К нему стоит относиться, как к помощнику для REST запросов. Этот помощник умеет решать ряд проблем и предоставляет удобный инструментарий для работы с кеш, что позволяет избавиться от большого количества повторяющегося кода. Однако в больших приложениях не все метаморфозы состояния линейны. Представим сценарий, что всему приложению нужна информация о пользователе. При этом гет запрос за пользователем зависит от нескольких параметров (locationId, distributionId и тд). Чтобы получить часть состояния с этим пользователем в RTK query, необходимо знать все параметры. Что делать если их неудобно получать в контейнере, которому нужна информация о пользователе? Если контейнер, делающий запрос за пользователем, уже не на странице? Если понадобится только id последнего полученного юзера? В таких случаях информацию стоит хранить в стандартном слайсе redux-toolkit и получать обычными селекторами, не перегружая код и умы разработчиков.

В итоге RTK query:

  • помог уменьшить количество кода для работы с состоянием приложения;

  • избавил нас от бойлерплейтов и кастомного кода при трекинге состояний и результатов запросов;

  • решил проблему одновременных запросов за одними и теми же данными;

  • из коробки позволил удобно работать с кеширования полученных данных на клиенте;

  • удобно реализует pessimistic/optimistic обновления состояний.


ссылка на оригинал статьи https://habr.com/ru/company/netcracker/blog/646163/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *