diff --git a/src/app/providers/StoreProvider/config/StateSchema.ts b/src/app/providers/StoreProvider/config/StateSchema.ts index 2186cab..43e1617 100644 --- a/src/app/providers/StoreProvider/config/StateSchema.ts +++ b/src/app/providers/StoreProvider/config/StateSchema.ts @@ -1,10 +1,27 @@ 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/reducerManager.ts b/src/app/providers/StoreProvider/config/reducerManager.ts new file mode 100644 index 0000000..a12d37b --- /dev/null +++ b/src/app/providers/StoreProvider/config/reducerManager.ts @@ -0,0 +1,42 @@ +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/store.ts b/src/app/providers/StoreProvider/config/store.ts index 7714507..4fa8020 100644 --- a/src/app/providers/StoreProvider/config/store.ts +++ b/src/app/providers/StoreProvider/config/store.ts @@ -1,19 +1,29 @@ -import { configureStore, ReducersMapObject } from '@reduxjs/toolkit'; +import { configureStore, DeepPartial, ReducersMapObject } from '@reduxjs/toolkit'; import { counterReducer } from 'entities/Counter'; 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/index.ts b/src/app/providers/StoreProvider/index.ts index 2955c5a..bb840b2 100644 --- a/src/app/providers/StoreProvider/index.ts +++ b/src/app/providers/StoreProvider/index.ts @@ -1,5 +1,5 @@ import { StoreProvider } from 'app/providers/StoreProvider/ui/StoreProvider'; import { createReduxStore } from 'app/providers/StoreProvider/config/store'; -import type { StateSchema } from 'app/providers/StoreProvider/config/StateSchema'; +import type { StateSchema, ReduxStoreWithManager } from 'app/providers/StoreProvider/config/StateSchema'; export { StoreProvider, createReduxStore, StateSchema }; diff --git a/src/app/providers/StoreProvider/ui/StoreProvider.tsx b/src/app/providers/StoreProvider/ui/StoreProvider.tsx index 3fd1347..dd316db 100644 --- a/src/app/providers/StoreProvider/ui/StoreProvider.tsx +++ b/src/app/providers/StoreProvider/ui/StoreProvider.tsx @@ -2,20 +2,25 @@ import { ReactNode } from 'react'; import { Provider } from 'react-redux'; import { createReduxStore } from 'app/providers/StoreProvider/config/store'; import { StateSchema } from 'app/providers/StoreProvider/config/StateSchema'; -import { DeepPartial } from '@reduxjs/toolkit'; +import { DeepPartial, ReducersMapObject } from '@reduxjs/toolkit'; interface StoreProviderProps { children?: ReactNode | any; initialState?: DeepPartial; + asyncReducers?: DeepPartial> } export const StoreProvider = (props: StoreProviderProps) => { const { children, initialState, + asyncReducers, } = props; - const store = createReduxStore(initialState as StateSchema); + const store = createReduxStore( + initialState as StateSchema, + asyncReducers as ReducersMapObject, + ); return ( {children} diff --git a/src/features/AuthByUsername/index.ts b/src/features/AuthByUsername/index.ts index f5c3e50..ecf6fba 100644 --- a/src/features/AuthByUsername/index.ts +++ b/src/features/AuthByUsername/index.ts @@ -1,3 +1,2 @@ export { LoginModal } from './ui/LoginModal/LoginModal'; export { LoginSchema } from './model/types/loginSchema'; -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..f5d4580 --- /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/getLoginState/getLoginState.ts b/src/features/AuthByUsername/model/selectors/getLoginState/getLoginState.ts deleted file mode 100644 index 2ebf931..0000000 --- a/src/features/AuthByUsername/model/selectors/getLoginState/getLoginState.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { StateSchema } from 'app/providers/StoreProvider'; - -export const getLoginState = (state: StateSchema) => state?.loginForm; 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/ui/LoginForm/LoginForm.async.ts b/src/features/AuthByUsername/ui/LoginForm/LoginForm.async.ts new file mode 100644 index 0000000..621e8bb --- /dev/null +++ b/src/features/AuthByUsername/ui/LoginForm/LoginForm.async.ts @@ -0,0 +1,8 @@ +import { FC, lazy } from 'react'; +import { LoginFormProps } from 'features/AuthByUsername/ui/LoginForm/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 de5982c..1da7149 100644 --- a/src/features/AuthByUsername/ui/LoginForm/LoginForm.stories.tsx +++ b/src/features/AuthByUsername/ui/LoginForm/LoginForm.stories.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { ComponentMeta, ComponentStory } from '@storybook/react'; import { StoreDecorator } from 'shared/config/storybook/StoreDecorator/StoreDecorator'; -import { LoginForm } from './LoginForm'; +import LoginForm from 'features/AuthByUsername/ui/LoginForm/LoginForm'; export default { title: 'features/LoginForm', diff --git a/src/features/AuthByUsername/ui/LoginForm/LoginForm.tsx b/src/features/AuthByUsername/ui/LoginForm/LoginForm.tsx index beacec0..dba872f 100644 --- a/src/features/AuthByUsername/ui/LoginForm/LoginForm.tsx +++ b/src/features/AuthByUsername/ui/LoginForm/LoginForm.tsx @@ -2,26 +2,35 @@ 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 { memo, useCallback } from 'react'; +import { useDispatch, useSelector, useStore } from 'react-redux'; +import { memo, useCallback, useEffect } from 'react'; import { Text, TextTheme } from 'shared/ui/Text/Text'; -import { loginByUsername } from '../../model/services/loginByUsername/loginByUsername'; -import { getLoginState } from '../../model/selectors/getLoginState/getLoginState'; -import { loginActions } from '../../model/slice/loginSlice'; +import { ReduxStoreWithManager } from 'app/providers/StoreProvider/config/StateSchema'; +import { DynamicModuleLoader, ReducersList } from 'shared/lib/components/DynamicModuleLoared/DynamicModuleLoared'; +import { getLoginUsername } from '../../model/selectors/getLoginUsername/getLoginUsername'; +import { getLoginIsLoading } from '../../model/selectors/getLoginIsLoading/getLoginIsLoading'; +import { getLoginPassword } from '../../model/selectors/getLoginPassword/getLoginPassword'; +import { getLoginError } from '../../model/selectors/getLoginError/getLoginError'; import cls from './LoginForm.module.scss'; +import { loginActions, loginReducer } from '../../model/slice/loginSlice'; +import { loginByUsername } from '../../model/services/loginByUsername/loginByUsername'; -interface LoginFormProps { +export interface LoginFormProps { className?: string; } -export const LoginForm = memo(({ className }: LoginFormProps) => { - const { t } = useTranslation(); +const initialReducers: ReducersList = { + loginForm: loginReducer, +}; +const LoginForm = memo(({ className }: LoginFormProps) => { + const { t } = useTranslation(); const dispatch = useDispatch(); - - const { - username, password, error, isLoading, - } = useSelector(getLoginState); + const store = useStore() as ReduxStoreWithManager; + const username = useSelector(getLoginUsername); + const password = useSelector(getLoginPassword); + const isLoading = useSelector(getLoginIsLoading); + const error = useSelector(getLoginError); const onChangeUsername = useCallback( (value: string) => { @@ -45,33 +54,41 @@ export const LoginForm = memo(({ className }: LoginFormProps) => { ); return ( -
- - {error && } - - + +
+ + {error && } + + + + +
+
- -
); }); + +export default LoginForm; diff --git a/src/features/AuthByUsername/ui/LoginModal/LoginModal.tsx b/src/features/AuthByUsername/ui/LoginModal/LoginModal.tsx index aeca86f..983c55e 100644 --- a/src/features/AuthByUsername/ui/LoginModal/LoginModal.tsx +++ b/src/features/AuthByUsername/ui/LoginModal/LoginModal.tsx @@ -1,5 +1,7 @@ import { Modal } from 'shared/ui/Modal/Modal'; -import { LoginForm } from 'features/AuthByUsername/ui/LoginForm/LoginForm'; +import { Suspense } from 'react'; +import { PageLoader } from 'shared/ui/PageLoader/PageLoader'; +import { LoginFormAsync } from '../LoginForm/LoginForm.async'; interface LoginModalProps { className?: string; @@ -9,6 +11,8 @@ interface LoginModalProps { 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 fcd86d0..1a13668 100644 --- a/src/shared/config/storybook/StoreDecorator/StoreDecorator.tsx +++ b/src/shared/config/storybook/StoreDecorator/StoreDecorator.tsx @@ -1,9 +1,21 @@ import { Story } from '@storybook/react'; import { StateSchema, StoreProvider } from 'app/providers/StoreProvider'; -import { DeepPartial } from '@reduxjs/toolkit'; +import { DeepPartial, ReducersMapObject } from '@reduxjs/toolkit'; +import loginForm from 'features/AuthByUsername/ui/LoginForm/LoginForm'; +import { loginReducer } from 'features/AuthByUsername/model/slice/loginSlice'; -export const StoreDecorator = (state: DeepPartial) => (StoryComponent: Story) => ( - +const defaultAsyncReducers:DeepPartial> = { + loginForm: loginReducer, +}; + +export const StoreDecorator = ( + state: DeepPartial, + asyncReducer?: DeepPartial>, +) => (StoryComponent: Story) => ( + ); diff --git a/src/shared/lib/components/DynamicModuleLoared/DynamicModuleLoared.tsx b/src/shared/lib/components/DynamicModuleLoared/DynamicModuleLoared.tsx new file mode 100644 index 0000000..9730610 --- /dev/null +++ b/src/shared/lib/components/DynamicModuleLoared/DynamicModuleLoared.tsx @@ -0,0 +1,53 @@ +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, reducer]: 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/widgets/Navbar/ui/Navbar.tsx b/src/widgets/Navbar/ui/Navbar.tsx index 784ce85..81ec1a3 100644 --- a/src/widgets/Navbar/ui/Navbar.tsx +++ b/src/widgets/Navbar/ui/Navbar.tsx @@ -52,7 +52,7 @@ export const Navbar = ({ className }: NavbarProps) => { > {t('login')} - + {isAuthModal && } ); };