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