В приложениях с 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/
Добавить комментарий