Skip to content

Commit

Permalink
Optimization. Asynchronous redusers. Bandle size
Browse files Browse the repository at this point in the history
  • Loading branch information
Bender101 committed Aug 25, 2023
1 parent ba09ec5 commit 9ee7e36
Show file tree
Hide file tree
Showing 19 changed files with 296 additions and 84 deletions.
47 changes: 47 additions & 0 deletions src/app/providers/StoreProvider/config/reducerManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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);
},
};
}
24 changes: 23 additions & 1 deletion src/app/providers/StoreProvider/config/stateSchema.ts
Original file line number Diff line number Diff line change
@@ -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<StateSchema>;
reduce: (state: StateSchema, action: AnyAction) => CombinedState<StateSchema>;
add: (key: StateSchemaKey, reducer: Reducer) => void;
remove: (key: StateSchemaKey) => void;
}

export interface ReduxStoreWithManager extends EnhancedStore<StateSchema> {
reducerManager: ReducerManager;
}
27 changes: 20 additions & 7 deletions src/app/providers/StoreProvider/config/store.ts
Original file line number Diff line number Diff line change
@@ -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<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;
}
21 changes: 12 additions & 9 deletions src/app/providers/StoreProvider/ui/StoreProvider.tsx
Original file line number Diff line number Diff line change
@@ -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<StateSchema>;
asyncReducers?: DeepPartial<ReducersMapObject<StateSchema>>;
}

export const StoreProvider: FC<StoreProviderProps> = ({
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<StateSchema>
);

return <Provider store={store}>{children}</Provider>;
};
2 changes: 1 addition & 1 deletion src/app/styles/variables/global.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@

// Цвета
--overlay-color: rgba(0 0 0 / 60%);
--red-light: red;
--red-light: #f00;
--red-dark: #c30b0b;
}
2 changes: 1 addition & 1 deletion src/features/AuthByUsername/index.ts
Original file line number Diff line number Diff line change
@@ -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";
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 || '';
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 || '';
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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");
}
});
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down
11 changes: 11 additions & 0 deletions src/features/AuthByUsername/ui/LoginForm/LoginForm.async.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { FC, lazy } from "react";
import { LoginFormProps } from "./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,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<typeof LoginForm> = {
Expand Down
98 changes: 60 additions & 38 deletions src/features/AuthByUsername/ui/LoginForm/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -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]
);

Expand All @@ -34,33 +52,37 @@ export const LoginForm = memo(({ className }: LoginFormProps) => {
}, [dispatch, password, username]);

return (
<div className={classNames(cls.loginForm, {}, [className])}>
<Text title={t("auth_form")} />
{error && (
<Text text={t('auth_error')} theme={TextTheme.ERROR}>
{error}
</Text>
)}
<Input
className={cls.input}
placeholder={t("username")}
onChange={onChangeUsername}
value={username}
/>
<Input
className={cls.input}
placeholder={t("password")}
onChange={onChangePassword}
value={password}
/>
<Button
disabled={isLoading}
theme={ButtonTheme.OUTLINE}
className={classNames(cls.loginBtn, {}, [className])}
onClick={onLoginClick}
>
{t("sign_in")}
</Button>
</div>
<DynamicModuleLoader removeAfterUnmount reducers={initialReducers}>
<div className={classNames(cls.loginForm, {}, [className])}>
<Text title={t("auth_form")} />
{error && (
<Text text={t("auth_error")} theme={TextTheme.ERROR}>
{error}
</Text>
)}
<Input
className={cls.input}
placeholder={t("username")}
onChange={onChangeUsername}
value={username}
/>
<Input
className={cls.input}
placeholder={t("password")}
onChange={onChangePassword}
value={password}
/>
<Button
disabled={isLoading}
theme={ButtonTheme.OUTLINE}
className={classNames(cls.loginBtn, {}, [className])}
onClick={onLoginClick}
>
{t("sign_in")}
</Button>
</div>
</DynamicModuleLoader>
);
});

export default LoginForm;
25 changes: 16 additions & 9 deletions src/features/AuthByUsername/ui/LoginModal/LoginModal.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
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;
isOpen: boolean;
onClose: () => void;
}

export const LoginModal: FC<LoginModalProps> = ({ onClose, isOpen }) => {
return (
<Modal isOpen={isOpen} onClose={onClose}>
<LoginForm />
</Modal>
);
};
export const LoginModal = ({ className, isOpen, onClose }: LoginModalProps) => (
<Modal
className={classNames("", {}, [className])}
isOpen={isOpen}
onClose={onClose}
lazy
>
<Suspense fallback={<Loader />}>
<LoginFormAsync />
</Suspense>
</Modal>
);
Loading

0 comments on commit 9ee7e36

Please sign in to comment.