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