Перейти к содержанию

State Management

Redux Toolkit + RTK Query для серверного состояния и кэширования.

Store

frontend/src/store/store.ts
import { configureStore } from '@reduxjs/toolkit';
import { combineReducers } from 'redux';
import { crudApi } from '@services/api/crudApi';
import { authApi } from '@services/auth/auth';
import authSlice from '@slices/authSlice';

const reducers = combineReducers({
    [crudApi.reducerPath]: crudApi.reducer,
    [authApi.reducerPath]: authApi.reducer,
    auth: authSlice,
});

export const store = configureStore({
    reducer: reducers,
    middleware: (getDefaultMiddleware) =>
        getDefaultMiddleware()
            .concat(crudApi.middleware)
            .concat(authApi.middleware),
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

RTK Query

Главная идея: серверное состояние — через RTK Query (автокэширование, инвалидация). Клиентское состояние — через slices (auth, UI state).

crudApi — универсальный CRUD

Один API-клиент для всех моделей:

frontend/src/services/api/crudApi.ts
export const crudApi = createApi({
    reducerPath: 'crudApi',
    baseQuery: baseQueryWithReauth,
    tagTypes: ['Fields', 'SavedFilters', 'Chat'],
    endpoints: build => ({

        search: build.query<GetListResult, GetListParams>({
            query: (arg) => ({
                method: 'POST',
                url: `/${arg.model}/search`,
                body: {
                    filter: arg.filter,
                    fields: arg.fields,
                    limit: arg.limit,
                    order: arg.order,
                    sort: arg.sort,
                },
            }),
        }),

        read: build.query<ReadResult, ReadParams>({
            query: (arg) => ({
                url: `/${arg.model}/read/${arg.id}`,
                params: { fields: arg.fields?.join(',') },
            }),
        }),

        create: build.mutation<CreateResult, CreateParams>({
            query: (arg) => ({
                method: 'POST',
                url: `/${arg.model}/create`,
                body: arg.data,
            }),
        }),

        edit: build.mutation<EditResult, EditParams>({
            query: (arg) => ({
                method: 'PATCH',
                url: `/${arg.model}/update/${arg.id}`,
                body: arg.data,
            }),
        }),

        deleteList: build.mutation<DeleteListResult, DeleteListParams>({
            query: (arg) => ({
                method: 'DELETE',
                url: `/${arg.model}/delete`,
                body: { ids: arg.ids },
            }),
        }),
    }),
});

Использование в компонентах

import { crudApi } from '@services/api/crudApi';

function ProductList() {
    const { data, isLoading } = crudApi.useSearchQuery({
        model: 'products',
        filter: [['active', '=', true]],
        fields: ['id', 'name', 'price'],
        limit: 20,
    });

    if (isLoading) return <Loader />;

    return (
        <DataTable
            records={data?.data ?? []}
            columns={[
                { accessor: 'name', title: 'Название' },
                { accessor: 'price', title: 'Цена' },
            ]}
        />
    );
}

Auth Slice

frontend/src/slices/authSlice.ts
const authSlice = createSlice({
    name: 'auth',
    initialState: { token: null, user: null },
    reducers: {
        setCredentials: (state, action) => {
            state.token = action.payload.token;
            state.user = action.payload.user;
        },
        logout: (state) => {
            state.token = null;
            state.user = null;
        },
    },
});