35 Оптимизация. Асинхронные редюсеры. Размер бандла
На данный момент у нас хранится абсолютно весь стор в приложении, что очень утяжеляет размер бандла и замедляет работу приложения. Чтобы мы могли реализовать код сплиттинг в рамках редакса, нам потребуется немного переработать стор таким образом, чтобы он собирался из менеджера редьюсеров
Первым делом нужно написать типы, которые будут использоваться в нашем стейте:
src / shared / types / state.schema.ts
import {
AnyAction,
CombinedState,
Reducer,
ReducersMapObject,
EnhancedStore,
Action,
MiddlewareArray,
ThunkMiddleware,
} from '@reduxjs/toolkit';
import { ToolkitStore } from '@reduxjs/toolkit/dist/configureStore';
import { BaseAuthSchema } from '@/features/BaseAuth';
import { UserSchema } from '@/entities/User';
import { rtkApi } from '@/shared/api';
export interface StateSchema {
/** базовые редьюсеры */
user: UserSchema;
[rtkApi.reducerPath]: ReturnType<typeof rtkApi.reducer>;
/** асинхронные редьюсеры */
baseAuth?: BaseAuthSchema;
}
/** все ключи схемы стейта */
export type StateSchemaKey = keyof StateSchema;
/** список вмонтированных редьюсеров */
export type MountedReducers = OptionalRecord<StateSchemaKey, boolean>;
export type ToolkitStoreType = ToolkitStore<
StateSchema,
Action<unknown>,
MiddlewareArray<[ThunkMiddleware<StateSchema, AnyAction, ThunkExtraArg>]>
/** костыль для добавления менеджера редьюсеров */
> & { reducerManager?: object };
/** возвращаемые значения из менеджера редьюсеров */
export interface ReducerManager {
getReducerMap: () => ReducersMapObject<StateSchema>;
reduce: (state: StateSchema, action: AnyAction) => CombinedState<StateSchema>;
add: (key: StateSchemaKey, reducer: Reducer) => void;
remove: (key: StateSchemaKey) => void;
getMountedReducers: () => MountedReducers;
}
/** дополнительные аргументы для thunk под наш кастомный фетч */
export interface ThunkExtraArg {
api: object;
}
/** тип стора редакса с менеджером редьюсеров */
export interface ReduxStoreWithManager extends EnhancedStore<StateSchema> {
reducerManager: ReducerManager;
}
/** тип конфига для асинхронного thunk */
export interface ThunkConfig<T> {
rejectValue: T;
extra: ThunkExtraArg;
state: StateSchema;
}
Далее пишем сам менеджер редьюсеров, логика которого взята из доки по редаксу
src / app / providers / StoreProvider / config / reducerManager.ts
import { AnyAction, combineReducers, Reducer, ReducersMapObject } from '@reduxjs/toolkit';
import {
MountedReducers,
ReducerManager,
StateSchema,
StateSchemaKey,
} from '@/shared/types/state.schema';
/**
* данный менеджер находится в документации Code Splitting Redux
* @param {ReducersMapObject<StateSchema>} initialReducers - начальные редьюсеры в приложении
* @returns {ReducerManager} manager - функциональность для менеджера редьюсеров
* */
export function createReducerManager(
initialReducers: ReducersMapObject<StateSchema>,
): ReducerManager {
/** собираем новый объект из редьюсеров */
const reducers = { ...initialReducers };
/** комбинированный стейт из переданных редьюсеров */
let combinedReducer = combineReducers(reducers);
/** список редьюсеров для удаления из стейта */
let keysToRemove: StateSchemaKey[] = [];
/** список вмонтированных редьюсеров */
const mountedReducers: MountedReducers = {};
return {
/** получение списка всех редьюсеров */
getReducerMap: () => reducers,
/**
* Получаем список вмонтированных редьюсеров
* true - вмонтирован, false - демонтирован
* */
getMountedReducers: () => mountedReducers,
/** возвращает стейт */
reduce: (state: StateSchema, action: AnyAction) => {
/** если в массиве есть список редьюсеров на удаление, то вырезаем их */
if (keysToRemove.length > 0) {
state = { ...state };
keysToRemove.forEach((key) => {
delete state[key];
});
keysToRemove = [];
}
/** возвращаем скобинированный редьюсер */
return combinedReducer(state, action);
},
/** добавление редьюсера */
add: (key: StateSchemaKey, reducer: Reducer) => {
if (!key || reducers[key]) {
return;
}
reducers[key] = reducer;
mountedReducers[key] = true;
combinedReducer = combineReducers(reducers);
},
/** удаление редьюсера */
remove: (key: StateSchemaKey) => {
if (!key || !reducers[key]) {
return;
}
delete reducers[key];
keysToRemove.push(key);
mountedReducers[key] = false;
combinedReducer = combineReducers(reducers);
},
};
}
Далее нам нужно будет обогатить стор, который мы собираем в редаксе и собирать его редьюсеры через редьюсер менеджер
src / app / providers / StoreProvider / config / store.ts
import { configureStore, ReducersMapObject } from '@reduxjs/toolkit';
import { CurriedGetDefaultMiddleware } from '@reduxjs/toolkit/dist/getDefaultMiddleware';
import { CombinedState, Reducer } from 'redux';
import { createReducerManager } from '@/app/providers/StoreProvider/config/reducerManager';
import { userReducers } from '@/entities/User';
import { rtkApi } from '@/shared/api';
import { StateSchema, ThunkExtraArg, ToolkitStoreType } from '@/shared/types';
/**
* Функция сборки редакс-стейта * @param {StateSchema} initialState - начальный стейт, который мы получаем извне (для тестов)
* @param {ReducersMapObject<StateSchema>} asyncReducers - асинхронные редьюсеры (для тестов)
* @returns store - стор редакса
* */
export const createReduxStore = (
initialState?: StateSchema,
asyncReducers?: ReducersMapObject<StateSchema>,
): ToolkitStoreType => {
/** импортируем все редьюсеры приложения сюда */
const reducers: ReducersMapObject<StateSchema> = {
...asyncReducers,
/**
* тут нужно оставлять только обязательные редьюсеры,
* которые нужны для начальной загрузки приложения
* */
user: userReducers,
[rtkApi.reducerPath]: rtkApi.reducer,
};
/** тут декларируем дополнительные аргументы в thunk, которые понядобятся для API */
const extraArgument: ThunkExtraArg = { api: {} };
/** инстанциируем менеджер редьюсеров */
const reducerManager = createReducerManager(reducers);
const store: ToolkitStoreType = configureStore({
/** берём из менеджера редьюсеры */
reducer: reducerManager.reduce as Reducer<CombinedState<StateSchema>>,
preloadedState: initialState,
devTools: __IS_DEV__ ? { shouldHotReload: true } : false,
middleware: (getDefaultMiddleware: CurriedGetDefaultMiddleware<StateSchema>) =>
getDefaultMiddleware({ thunk: { extraArgument } }),
});
/** добавляем в стейт сам менеджер редьюсеров */
store.reducerManager = reducerManager;
return store;
};
export type TRootState = ReturnType<typeof createReduxStore>['getState'];
export type TAppDispatch = ReturnType<typeof createReduxStore>['dispatch'];
А сейчас мы можем сделать неиспользуемые постоянно редьюсеры необязательными, чтобы стор собирался и без них
src / shared / lib / types / state.schema.ts
export interface StateSchema {
user: UserSchema;
baseAuth?: BaseAuthSchema;
[rtkApi.reducerPath]: ReturnType<typeof rtkApi.reducer>;
}
Сейчас у нас появляется возможность подгружать редьюсеры стейта асинхронно.
src / features / BaseAuth / ui / BaseAuth.tsx
export const BaseAuth = () => {
const store = useStore() as ReduxStoreWithManager;
useEffect(() => {
store.reducerManager.add('baseAuth', userReducers);
return () => {
store.reducerManager.remove('baseAuth');
};
}, [store]);
return <div></div>;
};
Далее нужно реализовать ХОК, который будет собирать все редьюсеры в стор динамически просто обернув в него компонент
src / shared / lib / components / DynamicModuleLoader / DynamicModuleLoader.tsx
import React, { ReactNode, useEffect } from 'react';
import { useStore } from 'react-redux';
import { Reducer } from 'redux';
import { useAppDispatch } from '@/shared/lib';
import { ReduxStoreWithManager, StateSchema, StateSchemaKey } from '@/shared/types';
export type ReducerList = {
[name in StateSchemaKey]?: Reducer<NonNullable<StateSchema[name]>>;
};
interface IDynamicModuleLoaderProps {
children: ReactNode;
/** список редьюсеров, которые нужно замаунтить */
reducers: ReducerList;
/** убрать после размонтирования */
removeAfterUnmount?: boolean;
}
/** ХОК, который подгружает асинхронные редьюсеры в стейт компонента */
export const DynamicModuleLoader = ({
children,
reducers,
removeAfterUnmount = true,
}: IDynamicModuleLoaderProps) => {
const dispatch = useAppDispatch();
const store = useStore() as ReduxStoreWithManager;
useEffect(() => {
/** получаем список вмонтированных редьюсеров */
const mountedReducers = store.reducerManager.getMountedReducers();
/** проходимся по всем редьюсерам и добавляем их в стор */
Object.entries(reducers).forEach(([name, reducer]) => {
const mounted = mountedReducers[name as StateSchemaKey];
/** если данный редьюсер не вмонитрован */
if (!mounted) {
/** то монтируем данный редьюсер */
store.reducerManager.add(name as StateSchemaKey, reducer);
/** добавляем лог в консоли редакса */
dispatch({ type: `@INIT ${name} reducer` });
}
});
return () => {
if (removeAfterUnmount) {
/** удаляем нужный редьюсер при анмаунте компонента */
Object.entries(reducers).forEach(([name, reducer]) => {
store.reducerManager.remove(name as StateSchemaKey);
dispatch({ type: `@UNMOUNT ${name} reducer` });
});
}
};
/** сюда не нужно добавлять зависимости! */
/* eslint-disable-next-line */
}, []);
return <>{children}</>;
};
И таким образом применяем наш ХОК для подгрузки стейта редакса
src / features / BaseAuth / ui / BaseAuth.tsx
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useStore } from 'react-redux';
import { DynamicModuleLoader, ReducerList } from '@/shared/lib';
import { ReduxStoreWithManager } from '@/shared/types';
import { baseAuthReducer } from '../model/slice/baseAuth.slice';
const initialReducers: ReducerList = {
baseAuth: baseAuthReducer,
};
export const BaseAuth = () => {
const { t } = useTranslation();
const store = useStore() as ReduxStoreWithManager;
return (
<DynamicModuleLoader removeAfterUnmount reducers={initialReducers}>
<div>{t('Auth')}</div>
</DynamicModuleLoader>
);
};
36 Тестирование фичи authByUsername. TestAsyncThunk
37 Страница профиля. Оптимизация перерисовок. Учимся использовать memo
38 Инстанс API. ApiUrl
39 Модуль профиля. Фетчинг данных. TS strict mode
40 Чиним типы и проект после TS strict mode. ThunkConfig