Pull to refresh
0

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

Reading time6 min
Views20K

В приложениях с 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 обновления состояний.

Tags:
Hubs:
Total votes 5: ↑5 and ↓0+5
Comments1

Articles

Information

Website
www.netcracker.com
Registered
Founded
Employees
5,001–10,000 employees
Location
США