From 9ee7e36368788e11e73aff81d110e9323b5bab8e Mon Sep 17 00:00:00 2001 From: bender101 Date: Fri, 25 Aug 2023 13:17:06 +0300 Subject: [PATCH] Optimization. Asynchronous redusers. Bandle size --- .../StoreProvider/config/reducerManager.ts | 47 +++++++++ .../StoreProvider/config/stateSchema.ts | 24 ++++- .../providers/StoreProvider/config/store.ts | 27 +++-- .../StoreProvider/ui/StoreProvider.tsx | 21 ++-- src/app/styles/variables/global.scss | 2 +- src/features/AuthByUsername/index.ts | 2 +- .../selectors/getLoginError/getLoginError.ts | 3 + .../getLoginIsLoading/getLoginIsLoading.ts | 3 + .../getLoginPassword/getLoginPassword.ts | 3 + .../getLoginUsername/getLoginUsername.ts | 3 + .../{ => model}/services/loginByUsername.ts | 10 +- .../slice/{LoginSlice.ts => loginSlice.ts} | 2 +- .../ui/LoginForm/LoginForm.async.ts | 11 +++ .../ui/LoginForm/LoginForm.stories.tsx | 2 +- .../AuthByUsername/ui/LoginForm/LoginForm.tsx | 98 ++++++++++++------- .../ui/LoginModal/LoginModal.tsx | 25 +++-- .../StoreDecorator/StoreDecorator.tsx | 25 +++-- .../lib/components/DynamicModuleLoader.tsx | 47 +++++++++ src/shared/ui/Modal/Modal.tsx | 25 ++++- 19 files changed, 296 insertions(+), 84 deletions(-) create mode 100644 src/app/providers/StoreProvider/config/reducerManager.ts create mode 100644 src/features/AuthByUsername/model/selectors/getLoginError/getLoginError.ts create mode 100644 src/features/AuthByUsername/model/selectors/getLoginIsLoading/getLoginIsLoading.ts create mode 100644 src/features/AuthByUsername/model/selectors/getLoginPassword/getLoginPassword.ts create mode 100644 src/features/AuthByUsername/model/selectors/getLoginUsername/getLoginUsername.ts rename src/features/AuthByUsername/{ => model}/services/loginByUsername.ts (71%) rename src/features/AuthByUsername/model/slice/{LoginSlice.ts => loginSlice.ts} (93%) create mode 100644 src/features/AuthByUsername/ui/LoginForm/LoginForm.async.ts create mode 100644 src/shared/lib/components/DynamicModuleLoader.tsx diff --git a/src/app/providers/StoreProvider/config/reducerManager.ts b/src/app/providers/StoreProvider/config/reducerManager.ts new file mode 100644 index 0000000..105290d --- /dev/null +++ b/src/app/providers/StoreProvider/config/reducerManager.ts @@ -0,0 +1,47 @@ +import { + AnyAction, + combineReducers, + Reducer, + ReducersMapObject, +} from "@reduxjs/toolkit"; +import { ReducerManager, StateSchema, StateSchemaKey } from "./StateSchema"; + +export function createReducerManager( + initialReducers: ReducersMapObject +): ReducerManager { + const reducers = { ...initialReducers }; + + let combinedReducer = combineReducers(reducers); + + let keysToRemove: Array = []; + + return { + getReducerMap: () => reducers, + 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; + + combinedReducer = combineReducers(reducers); + }, + remove: (key: StateSchemaKey) => { + if (!key || !reducers[key]) { + return; + } + delete reducers[key]; + keysToRemove.push(key); + combinedReducer = combineReducers(reducers); + }, + }; +} diff --git a/src/app/providers/StoreProvider/config/stateSchema.ts b/src/app/providers/StoreProvider/config/stateSchema.ts index 86cb2d7..8a5f876 100644 --- a/src/app/providers/StoreProvider/config/stateSchema.ts +++ b/src/app/providers/StoreProvider/config/stateSchema.ts @@ -1,9 +1,31 @@ import { CounterSchema } from "entities/Counter"; import { UserSchema } from "entities/User"; import { LoginSchema } from "features/AuthByUsername"; +import { + AnyAction, + EnhancedStore, + Reducer, + ReducersMapObject, +} from "@reduxjs/toolkit"; +import { CombinedState } from "redux"; export interface StateSchema { counter: CounterSchema; user: UserSchema; - loginForm: LoginSchema; + + // Асинхронные редюсеры + loginForm?: LoginSchema; +} + +export type StateSchemaKey = keyof StateSchema; + +export interface ReducerManager { + getReducerMap: () => ReducersMapObject; + reduce: (state: StateSchema, action: AnyAction) => CombinedState; + add: (key: StateSchemaKey, reducer: Reducer) => void; + remove: (key: StateSchemaKey) => void; +} + +export interface ReduxStoreWithManager extends EnhancedStore { + reducerManager: ReducerManager; } diff --git a/src/app/providers/StoreProvider/config/store.ts b/src/app/providers/StoreProvider/config/store.ts index 8f7217b..119dd59 100644 --- a/src/app/providers/StoreProvider/config/store.ts +++ b/src/app/providers/StoreProvider/config/store.ts @@ -1,19 +1,32 @@ -import { configureStore, ReducersMapObject } from "@reduxjs/toolkit"; +import { + configureStore, + ReducersMapObject, +} from "@reduxjs/toolkit"; import { counterReducer } from "entities/Counter"; -import { StateSchema } from "./stateSchema"; import { userReducer } from "entities/User"; -import { loginReducer } from "features/AuthByUsername"; +import { StateSchema } from "./StateSchema"; +import { createReducerManager } from "./reducerManager"; -export function createReduxStore(initialState: StateSchema) { +export function createReduxStore( + initialState?: StateSchema, + asyncReducers?: ReducersMapObject +) { const rootReducers: ReducersMapObject = { + ...asyncReducers, counter: counterReducer, user: userReducer, - loginForm: loginReducer, }; - return configureStore({ - reducer: rootReducers, + const reducerManager = createReducerManager(rootReducers); + + const store = configureStore({ + reducer: reducerManager.reduce, devTools: __IS_DEV__, preloadedState: initialState, }); + + // @ts-ignore + store.reducerManager = reducerManager; + + return store; } diff --git a/src/app/providers/StoreProvider/ui/StoreProvider.tsx b/src/app/providers/StoreProvider/ui/StoreProvider.tsx index d988bf8..3ff6d0c 100644 --- a/src/app/providers/StoreProvider/ui/StoreProvider.tsx +++ b/src/app/providers/StoreProvider/ui/StoreProvider.tsx @@ -1,19 +1,22 @@ -import { FC, ReactNode } from "react"; +import { ReactNode } from "react"; import { Provider } from "react-redux"; -import { createReduxStore } from "../index"; -import { StateSchema } from "../config/stateSchema"; -import { DeepPartial } from "redux"; +import { createReduxStore } from "app/providers/StoreProvider/config/store"; +import { StateSchema } from "app/providers/StoreProvider/config/StateSchema"; +import { DeepPartial, ReducersMapObject } from "@reduxjs/toolkit"; interface StoreProviderProps { children?: ReactNode; initialState?: DeepPartial; + asyncReducers?: DeepPartial>; } -export const StoreProvider: FC = ({ - children, - initialState, -}) => { - const store = createReduxStore(initialState as StateSchema); +export const StoreProvider = (props: StoreProviderProps) => { + const { children, initialState, asyncReducers } = props; + + const store = createReduxStore( + initialState as StateSchema, + asyncReducers as ReducersMapObject + ); return {children}; }; diff --git a/src/app/styles/variables/global.scss b/src/app/styles/variables/global.scss index e136b72..039f80c 100644 --- a/src/app/styles/variables/global.scss +++ b/src/app/styles/variables/global.scss @@ -20,6 +20,6 @@ // Цвета --overlay-color: rgba(0 0 0 / 60%); - --red-light: red; + --red-light: #f00; --red-dark: #c30b0b; } diff --git a/src/features/AuthByUsername/index.ts b/src/features/AuthByUsername/index.ts index edb781f..b1c18a5 100644 --- a/src/features/AuthByUsername/index.ts +++ b/src/features/AuthByUsername/index.ts @@ -1,3 +1,3 @@ export { LoginModal } from "./ui/LoginModal/LoginModal"; export { LoginSchema } from "./model/types/LoginSchema"; -export { loginReducer } from "./model/slice/LoginSlice"; +export { loginReducer } from "./model/slice/loginSlice"; diff --git a/src/features/AuthByUsername/model/selectors/getLoginError/getLoginError.ts b/src/features/AuthByUsername/model/selectors/getLoginError/getLoginError.ts new file mode 100644 index 0000000..22adac5 --- /dev/null +++ b/src/features/AuthByUsername/model/selectors/getLoginError/getLoginError.ts @@ -0,0 +1,3 @@ +import { StateSchema } from "app/providers/StoreProvider"; + +export const getLoginError = (state: StateSchema) => state?.loginForm?.error; diff --git a/src/features/AuthByUsername/model/selectors/getLoginIsLoading/getLoginIsLoading.ts b/src/features/AuthByUsername/model/selectors/getLoginIsLoading/getLoginIsLoading.ts new file mode 100644 index 0000000..7209506 --- /dev/null +++ b/src/features/AuthByUsername/model/selectors/getLoginIsLoading/getLoginIsLoading.ts @@ -0,0 +1,3 @@ +import { StateSchema } from 'app/providers/StoreProvider'; + +export const getLoginIsLoading = (state: StateSchema) => state?.loginForm?.isLoading || false; diff --git a/src/features/AuthByUsername/model/selectors/getLoginPassword/getLoginPassword.ts b/src/features/AuthByUsername/model/selectors/getLoginPassword/getLoginPassword.ts new file mode 100644 index 0000000..2e05034 --- /dev/null +++ b/src/features/AuthByUsername/model/selectors/getLoginPassword/getLoginPassword.ts @@ -0,0 +1,3 @@ +import { StateSchema } from 'app/providers/StoreProvider'; + +export const getLoginPassword = (state: StateSchema) => state?.loginForm?.password || ''; diff --git a/src/features/AuthByUsername/model/selectors/getLoginUsername/getLoginUsername.ts b/src/features/AuthByUsername/model/selectors/getLoginUsername/getLoginUsername.ts new file mode 100644 index 0000000..d20ce22 --- /dev/null +++ b/src/features/AuthByUsername/model/selectors/getLoginUsername/getLoginUsername.ts @@ -0,0 +1,3 @@ +import { StateSchema } from 'app/providers/StoreProvider'; + +export const getLoginUsername = (state: StateSchema) => state?.loginForm?.username || ''; diff --git a/src/features/AuthByUsername/services/loginByUsername.ts b/src/features/AuthByUsername/model/services/loginByUsername.ts similarity index 71% rename from src/features/AuthByUsername/services/loginByUsername.ts rename to src/features/AuthByUsername/model/services/loginByUsername.ts index 9ddd37e..3da2223 100644 --- a/src/features/AuthByUsername/services/loginByUsername.ts +++ b/src/features/AuthByUsername/model/services/loginByUsername.ts @@ -1,7 +1,7 @@ import { createAsyncThunk } from "@reduxjs/toolkit"; -import {User, userActions} from "entities/User"; import axios from "axios"; -import {USER_LOCALSTORAGE_KEY} from "shared/const/localStorage"; +import { User, userActions } from "entities/User"; +import { USER_LOCALSTORAGE_KEY } from "shared/const/localStorage"; interface LoginByUsernameProps { username: string; @@ -20,12 +20,12 @@ export const loginByUsername = createAsyncThunk< throw new Error(); } - localStorage.setItem(USER_LOCALSTORAGE_KEY, JSON.stringify(response.data)) - thunkAPI.dispatch(userActions.setAuthData(response.data)) + localStorage.setItem(USER_LOCALSTORAGE_KEY, JSON.stringify(response.data)); + thunkAPI.dispatch(userActions.setAuthData(response.data)); return response.data; } catch (e) { console.error(e); - return thunkAPI.rejectWithValue('error'); + return thunkAPI.rejectWithValue("error"); } }); diff --git a/src/features/AuthByUsername/model/slice/LoginSlice.ts b/src/features/AuthByUsername/model/slice/loginSlice.ts similarity index 93% rename from src/features/AuthByUsername/model/slice/LoginSlice.ts rename to src/features/AuthByUsername/model/slice/loginSlice.ts index abad952..5fcf85f 100644 --- a/src/features/AuthByUsername/model/slice/LoginSlice.ts +++ b/src/features/AuthByUsername/model/slice/loginSlice.ts @@ -1,5 +1,5 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { loginByUsername } from "../../services/loginByUsername"; +import { loginByUsername } from "../../model/services/loginByUsername"; import { LoginSchema } from "../types/LoginSchema"; const initialState: LoginSchema = { diff --git a/src/features/AuthByUsername/ui/LoginForm/LoginForm.async.ts b/src/features/AuthByUsername/ui/LoginForm/LoginForm.async.ts new file mode 100644 index 0000000..39f5185 --- /dev/null +++ b/src/features/AuthByUsername/ui/LoginForm/LoginForm.async.ts @@ -0,0 +1,11 @@ +import { FC, lazy } from "react"; +import { LoginFormProps } from "./LoginForm"; + +export const LoginFormAsync = lazy>( + () => + new Promise((resolve) => { + // @ts-ignore + // ТАК В РЕАЛЬНЫХ ПРОЕКТАХ НЕ ДЕЛАТЬ!!!!! ДЕЛАЕМ ДЛЯ КУРСА! + setTimeout(() => resolve(import("./LoginForm")), 1500); + }) +); diff --git a/src/features/AuthByUsername/ui/LoginForm/LoginForm.stories.tsx b/src/features/AuthByUsername/ui/LoginForm/LoginForm.stories.tsx index abc8c4d..23d9776 100644 --- a/src/features/AuthByUsername/ui/LoginForm/LoginForm.stories.tsx +++ b/src/features/AuthByUsername/ui/LoginForm/LoginForm.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { LoginForm } from "./LoginForm"; +import LoginForm from "./LoginForm"; import { StoreDecorator } from "shared/config/storybook/StoreDecorator/StoreDecorator"; const meta: Meta = { diff --git a/src/features/AuthByUsername/ui/LoginForm/LoginForm.tsx b/src/features/AuthByUsername/ui/LoginForm/LoginForm.tsx index bb61aa3..5cf27ad 100644 --- a/src/features/AuthByUsername/ui/LoginForm/LoginForm.tsx +++ b/src/features/AuthByUsername/ui/LoginForm/LoginForm.tsx @@ -1,31 +1,49 @@ -import cls from "./LoginForm.module.scss"; -import { memo, useCallback } from "react"; import { classNames } from "shared/lib/classNames/classNames"; import { useTranslation } from "react-i18next"; import { Button, ButtonTheme } from "shared/ui/Button/Button"; import { Input } from "shared/ui/Input/Input"; import { useDispatch, useSelector } from "react-redux"; -import { loginActions } from "../../model/slice/LoginSlice"; -import { getLoginState } from "../../model/selectors/getLoginState/getLoginState"; -import { loginByUsername } from "../../services/loginByUsername"; +import { memo, useCallback } from "react"; import { Text, TextTheme } from "shared/ui/Text/Text"; +import { getLoginUsername } from "../../model/selectors/getLoginUsername/getLoginUsername"; +import { getLoginPassword } from "../../model/selectors/getLoginPassword/getLoginPassword"; +import { getLoginIsLoading } from "../../model/selectors/getLoginIsLoading/getLoginIsLoading"; +import { getLoginError } from "../../model/selectors/getLoginError/getLoginError"; +import { loginActions, loginReducer } from "../../model/slice/loginSlice"; +import cls from "./LoginForm.module.scss"; +import { + DynamicModuleLoader, + ReducersList, +} from "shared/lib/components/DynamicModuleLoader"; +import { loginByUsername } from "../../model/services/loginByUsername"; -interface LoginFormProps { +export interface LoginFormProps { className?: string; } -export const LoginForm = memo(({ className }: LoginFormProps) => { +const initialReducers: ReducersList = { + loginForm: loginReducer, +}; + +const LoginForm = memo(({ className }: LoginFormProps) => { const { t } = useTranslation(); const dispatch = useDispatch(); - const { username, password, error, isLoading } = useSelector(getLoginState); + const username = useSelector(getLoginUsername); + const password = useSelector(getLoginPassword); + const isLoading = useSelector(getLoginIsLoading); + const error = useSelector(getLoginError); const onChangeUsername = useCallback( - (value: string) => dispatch(loginActions.setUsername(value)), + (value: string) => { + dispatch(loginActions.setUsername(value)); + }, [dispatch] ); const onChangePassword = useCallback( - (value: string) => dispatch(loginActions.setPassword(value)), + (value: string) => { + dispatch(loginActions.setPassword(value)); + }, [dispatch] ); @@ -34,33 +52,37 @@ export const LoginForm = memo(({ className }: LoginFormProps) => { }, [dispatch, password, username]); return ( -
- - {error && ( - - {error} - - )} - - - -
+ +
+ + {error && ( + + {error} + + )} + + + +
+
); }); + +export default LoginForm; diff --git a/src/features/AuthByUsername/ui/LoginModal/LoginModal.tsx b/src/features/AuthByUsername/ui/LoginModal/LoginModal.tsx index 46f4067..f9de90f 100644 --- a/src/features/AuthByUsername/ui/LoginModal/LoginModal.tsx +++ b/src/features/AuthByUsername/ui/LoginModal/LoginModal.tsx @@ -1,6 +1,8 @@ -import { FC } from "react"; import { Modal } from "shared/ui/Modal/Modal"; -import { LoginForm } from "../LoginForm/LoginForm"; +import { classNames } from "shared/lib/classNames/classNames"; +import { Suspense } from "react"; +import { Loader } from "shared/ui/Loader/Loader"; +import { LoginFormAsync } from "../LoginForm/LoginForm.async"; interface LoginModalProps { className?: string; @@ -8,10 +10,15 @@ interface LoginModalProps { onClose: () => void; } -export const LoginModal: FC = ({ onClose, isOpen }) => { - return ( - - - - ); -}; +export const LoginModal = ({ className, isOpen, onClose }: LoginModalProps) => ( + + }> + + + +); diff --git a/src/shared/config/storybook/StoreDecorator/StoreDecorator.tsx b/src/shared/config/storybook/StoreDecorator/StoreDecorator.tsx index 17d1041..ee9860b 100644 --- a/src/shared/config/storybook/StoreDecorator/StoreDecorator.tsx +++ b/src/shared/config/storybook/StoreDecorator/StoreDecorator.tsx @@ -1,12 +1,23 @@ -import { StoryFn } from "@storybook/react"; +import { Story } from "@storybook/react"; import { StateSchema, StoreProvider } from "app/providers/StoreProvider"; -import { DeepPartial } from "redux"; +import { DeepPartial, ReducersMapObject } from "@reduxjs/toolkit"; +import { loginReducer } from "features/AuthByUsername/model/slice/loginSlice"; + +const defaultAsyncReducers: DeepPartial> = { + loginForm: loginReducer, +}; export const StoreDecorator = - (state: DeepPartial) => (Story: StoryFn) => { - return ( - - + ( + state: DeepPartial, + asyncReducers?: DeepPartial> + ) => + (StoryComponent: Story) => + ( + + ); - }; diff --git a/src/shared/lib/components/DynamicModuleLoader.tsx b/src/shared/lib/components/DynamicModuleLoader.tsx new file mode 100644 index 0000000..9800aba --- /dev/null +++ b/src/shared/lib/components/DynamicModuleLoader.tsx @@ -0,0 +1,47 @@ +import { FC, useEffect } from "react"; +import { useDispatch, useStore } from "react-redux"; +import { + ReduxStoreWithManager, + StateSchemaKey, +} from "app/providers/StoreProvider/config/StateSchema"; +import { Reducer } from "@reduxjs/toolkit"; + +export type ReducersList = { + [name in StateSchemaKey]?: Reducer; +}; + +type ReducersListEntry = [StateSchemaKey, Reducer]; + +interface DynamicModuleLoaderProps { + reducers: ReducersList; + removeAfterUnmount?: boolean; +} + +export const DynamicModuleLoader: FC = (props) => { + const { children, reducers, removeAfterUnmount } = props; + + const store = useStore() as ReduxStoreWithManager; + const dispatch = useDispatch(); + + useEffect(() => { + Object.entries(reducers).forEach(([name, reducer]: ReducersListEntry) => { + store.reducerManager.add(name, reducer); + dispatch({ type: `@INIT ${name} reducer` }); + }); + + return () => { + if (removeAfterUnmount) { + Object.entries(reducers).forEach(([name]: ReducersListEntry) => { + store.reducerManager.remove(name); + dispatch({ type: `@DESTROY ${name} reducer` }); + }); + } + }; + // eslint-disable-next-line + }, []); + + return ( + // eslint-disable-next-line react/jsx-no-useless-fragment + <>{children} + ); +}; diff --git a/src/shared/ui/Modal/Modal.tsx b/src/shared/ui/Modal/Modal.tsx index 0cde774..5a5e65b 100644 --- a/src/shared/ui/Modal/Modal.tsx +++ b/src/shared/ui/Modal/Modal.tsx @@ -1,12 +1,14 @@ import { classNames } from "shared/lib/classNames/classNames"; -import React, { +import { ReactNode, useCallback, useEffect, useRef, useState, + MouseEvent, } from "react"; import { Portal } from "shared/ui/Portal/Portal"; +import { useTheme } from "app/providers/ThemeProvider"; import cls from "./Modal.module.scss"; interface ModalProps { @@ -14,15 +16,24 @@ interface ModalProps { children?: ReactNode; isOpen?: boolean; onClose?: () => void; + lazy?: boolean; } const ANIMATION_DELAY = 300; export const Modal = (props: ModalProps) => { - const { className, children, isOpen, onClose } = props; + const { className, children, isOpen, onClose, lazy } = props; const [isClosing, setIsClosing] = useState(false); + const [isMounted, setIsMounted] = useState(false); const timerRef = useRef>(); + const { theme } = useTheme(); + + useEffect(() => { + if (isOpen) { + setIsMounted(true); + } + }, [isOpen]); const closeHandler = useCallback(() => { if (onClose) { @@ -44,7 +55,7 @@ export const Modal = (props: ModalProps) => { [closeHandler] ); - const onContentClick = (e: React.MouseEvent) => { + const onContentClick = (e: MouseEvent) => { e.stopPropagation(); }; @@ -64,9 +75,15 @@ export const Modal = (props: ModalProps) => { [cls.isClosing]: isClosing, }; + if (lazy && !isMounted) { + return null; + } + return ( -
+
{children}