Skip to content

Commit

Permalink
feat: add reducer manager for async reducers
Browse files Browse the repository at this point in the history
  • Loading branch information
TomatoVan committed Nov 21, 2023
1 parent bfbedf9 commit e7bf3c7
Show file tree
Hide file tree
Showing 18 changed files with 236 additions and 60 deletions.
19 changes: 18 additions & 1 deletion src/app/providers/StoreProvider/config/StateSchema.ts
Original file line number Diff line number Diff line change
@@ -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<StateSchema>;
reduce: (state: StateSchema, action: AnyAction) => CombinedState<StateSchema>;

Check warning on line 20 in src/app/providers/StoreProvider/config/StateSchema.ts

View workflow job for this annotation

GitHub Actions / pipeline (17.x)

'state' is defined but never used

Check warning on line 20 in src/app/providers/StoreProvider/config/StateSchema.ts

View workflow job for this annotation

GitHub Actions / pipeline (17.x)

'action' is defined but never used
add: (key: StateSchemaKey, reducer: Reducer) => void;

Check warning on line 21 in src/app/providers/StoreProvider/config/StateSchema.ts

View workflow job for this annotation

GitHub Actions / pipeline (17.x)

'key' is defined but never used

Check warning on line 21 in src/app/providers/StoreProvider/config/StateSchema.ts

View workflow job for this annotation

GitHub Actions / pipeline (17.x)

'reducer' is defined but never used
remove: (key: StateSchemaKey) => void;

Check warning on line 22 in src/app/providers/StoreProvider/config/StateSchema.ts

View workflow job for this annotation

GitHub Actions / pipeline (17.x)

'key' is defined but never used
}

export interface ReduxStoreWithManager extends EnhancedStore<StateSchema> {
reducerManager: ReducerManager;
}
42 changes: 42 additions & 0 deletions src/app/providers/StoreProvider/config/reducerManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
AnyAction, combineReducers, Reducer, ReducersMapObject,
} from '@reduxjs/toolkit';
import { ReducerManager, StateSchema, StateSchemaKey } from './StateSchema';

export function createReducerManager(initialReducers: ReducersMapObject<StateSchema>): ReducerManager {
const reducers = { ...initialReducers };

let combinedReducer = combineReducers(reducers);

let keysToRemove: Array<StateSchemaKey> = [];

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);
},
};
}
22 changes: 16 additions & 6 deletions src/app/providers/StoreProvider/config/store.ts
Original file line number Diff line number Diff line change
@@ -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<StateSchema>,
) {
const rootReducers: ReducersMapObject<StateSchema> = {
...asyncReducers,
counter: counterReducer,
user: userReducer,
loginForm: loginReducer,
};

return configureStore<StateSchema>({
reducer: rootReducers,
const reducerManager = createReducerManager(rootReducers);

const store = configureStore<StateSchema>({
reducer: reducerManager.reduce,
devTools: __IS_DEV__,
preloadedState: initialState,
});

// @ts-ignore
store.reducerManager = reducerManager;

return store;
}
2 changes: 1 addition & 1 deletion src/app/providers/StoreProvider/index.ts
Original file line number Diff line number Diff line change
@@ -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 };
9 changes: 7 additions & 2 deletions src/app/providers/StoreProvider/ui/StoreProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<StateSchema>;
asyncReducers?: DeepPartial<ReducersMapObject<StateSchema>>
}

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<StateSchema>,
);
return (
<Provider store={store}>
{children}
Expand Down
1 change: 0 additions & 1 deletion src/features/AuthByUsername/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export { LoginModal } from './ui/LoginModal/LoginModal';
export { LoginSchema } from './model/types/loginSchema';
export { loginReducer } from './model/slice/loginSlice';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { StateSchema } from 'app/providers/StoreProvider';

export const getLoginError = (state: StateSchema) => state?.loginForm?.error;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { StateSchema } from 'app/providers/StoreProvider';

export const getLoginIsLoading = (state: StateSchema) => state?.loginForm?.isLoading || false;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { StateSchema } from 'app/providers/StoreProvider';

export const getLoginPassword = (state: StateSchema) => state?.loginForm?.password || '';

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { StateSchema } from 'app/providers/StoreProvider';

export const getLoginUsername = (state: StateSchema) => state?.loginForm?.username || '';
8 changes: 8 additions & 0 deletions src/features/AuthByUsername/ui/LoginForm/LoginForm.async.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { FC, lazy } from 'react';
import { LoginFormProps } from 'features/AuthByUsername/ui/LoginForm/LoginForm';

export const LoginFormAsync = lazy<FC<LoginFormProps>>(() => new Promise((resolve) => {
// @ts-ignore
// ТАК В РЕАЛЬНЫХ ПРОЕКТАХ НЕ ДЕЛАТЬ!!!!! ДЕЛАЕМ ДЛЯ КУРСА!
setTimeout(() => resolve(import('./LoginForm')), 1500);
}));
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
95 changes: 56 additions & 39 deletions src/features/AuthByUsername/ui/LoginForm/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -45,33 +54,41 @@ export const LoginForm = memo(({ className }: LoginFormProps) => {
);

return (
<div className={classNames(cls.LoginForm, {}, [className])}>
<Text title={t('auth_form')} />
{error && <Text text={t('login_error')} theme={TextTheme.ERROR} />}
<Input
autofocus
type="text"
className={cls.input}
placeholder={t('auth_username')}
onChange={onChangeUsername}
value={username}
/>
<Input
type="text"
className={cls.input}
placeholder={t('auth_password')}
onChange={onChangePassword}
value={password}
/>
<DynamicModuleLoader
removeAfterUnmount
reducers={initialReducers}
>
<div className={classNames(cls.LoginForm, {}, [className])}>
<Text title={t('auth_form')} />
{error && <Text text={t('login_error')} theme={TextTheme.ERROR} />}
<Input
autofocus
type="text"
className={cls.input}
placeholder={t('auth_username')}
onChange={onChangeUsername}
value={username}
/>
<Input
type="text"
className={cls.input}
placeholder={t('auth_password')}
onChange={onChangePassword}
value={password}
/>

<Button
onClick={onLoginClick}
theme={ButtonTheme.OUTLINE}
className={cls.loginBtn}
isDisabled={isLoading}
>
{t('login')}
</Button>
</div>
</DynamicModuleLoader>

<Button
onClick={onLoginClick}
theme={ButtonTheme.OUTLINE}
className={cls.loginBtn}
isDisabled={isLoading}
>
{t('login')}
</Button>
</div>
);
});

export default LoginForm;
8 changes: 6 additions & 2 deletions src/features/AuthByUsername/ui/LoginModal/LoginModal.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -9,6 +11,8 @@ interface LoginModalProps {

export const LoginModal = ({ className, isOpen, onClose }: LoginModalProps) => (
<Modal lazy onClose={onClose} isOpen={isOpen}>
<LoginForm />
<Suspense fallback={<PageLoader />}>
<LoginFormAsync />
</Suspense>
</Modal>
);
18 changes: 15 additions & 3 deletions src/shared/config/storybook/StoreDecorator/StoreDecorator.tsx
Original file line number Diff line number Diff line change
@@ -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<StateSchema>) => (StoryComponent: Story) => (
<StoreProvider initialState={state}>
const defaultAsyncReducers:DeepPartial<ReducersMapObject<StateSchema>> = {
loginForm: loginReducer,
};

export const StoreDecorator = (
state: DeepPartial<StateSchema>,
asyncReducer?: DeepPartial<ReducersMapObject<StateSchema>>,
) => (StoryComponent: Story) => (
<StoreProvider
initialState={state}
asyncReducers={{ ...defaultAsyncReducers, ...asyncReducer }}
>
<StoryComponent />
</StoreProvider>
);
Original file line number Diff line number Diff line change
@@ -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<DynamicModuleLoaderProps> = (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}
</>
);
};
Loading

0 comments on commit e7bf3c7

Please sign in to comment.