29 Модальное окно. Portal

Далее нужно реализовать модальное окно в приложении.

Первым делом нужно создать для него портал, который будет рисовать модальное окно из другой части приложения (конкретно нужно, чтобы портал рисовался вне контента - в body)

Пропсы:

src / shared / ui / Portal / ui / Portal.props.ts

import { ReactNode } from 'react';  
  
export interface IPortalProps {  
    /** компонент, который будет отрисовываться */  
    children: ReactNode;  
    /** конечная точка, в которой он должен отрисоваться */  
    element?: Element | DocumentFragment;  
}

Сам портал, который работает за счёт функции createPortal

src / shared / ui / Portal / ui / Portal.tsx

import { createPortal } from 'react-dom';  
import { IPortalProps } from './Portal.props';  
  
/** портал для рендера компонентов в отдельных частях приложения */  
export const Portal = ({ children, element = document.body }: IPortalProps) =>  
    createPortal(children, element);

Далее уже реализуем хук для работы модалки. Хук используется для того, чтобы сделать логику раскрытия переиспользуемой.

src / shared / lib / hooks / useModal.tsx

import React, {  
    MouseEventHandler,  
    MutableRefObject,  
    useCallback,  
    useEffect,  
    useRef,  
    useState,  
} from 'react';  
  
interface IModalProps {  
    /** начальное состояние модального окна */  
    isOpened?: boolean;  
}  
  
interface IModalResult {  
    /** реф на модальное окно */  
    modalRef: MutableRefObject<HTMLDivElement | null>;  
    /** функция открытия модального окна */  
    handleOpenModal: () => void;  
    /** состояние модального окна */  
    modalExpanded: boolean;  
    /** функция, которая останавливает всплытие события */  
    stopPropagation: MouseEventHandler<HTMLDivElement>;  
}  
  
/** хук логики модального окна */  
export const useModal = ({ isOpened = false }: IModalProps = {}): IModalResult => {  
    /** состояние отображения модального окна */  
    const [modalExpanded, setModalExpanded] = useState<boolean>(isOpened);  
  
    /** реф, от которого и будет работать закрытие модального окна */  
    const modalRef: MutableRefObject<HTMLDivElement | null> = useRef<HTMLDivElement | null>(null);  
  
    /** функция для изменения состояния окна */  
    const handleOpenModal = useCallback(() => setModalExpanded((prevState) => !prevState), []);  
    /** останавливает всплытие события */  
    const stopPropagation: MouseEventHandler<HTMLDivElement> = useCallback(  
       (e: React.MouseEvent<HTMLDivElement>) => e.stopPropagation(),  
       [],  
    );  
    /** функция закрытия модалки на Esc */  
    const onKeyDown = useCallback((e: KeyboardEvent) => {  
       if (e.key === 'Escape') {  
          setModalExpanded(false);  
       }  
    }, []);  
    /** отлавливает событие закрытия окна и закрывает модалку */  
    const handleModalClick = useCallback((event: MouseEvent) => {  
       if (modalRef.current && !modalRef.current?.contains(event.target as Node)) {  
          setModalExpanded(false);  
       }  
    }, []);  
  
    useEffect(() => {  
       document.addEventListener('click', handleModalClick);  
  
       if (modalExpanded) {  
          document.addEventListener('keydown', onKeyDown);  
       }  
  
       return () => {  
          document.removeEventListener('click', handleModalClick);  
          document.removeEventListener('keydown', onKeyDown);  
       };  
    }, [handleModalClick, modalExpanded, onKeyDown]);  
  
    return {  
       modalRef,  
       handleOpenModal,  
       modalExpanded,  
       stopPropagation,  
    };  
};

Пропсы модалки:

src / shared / ui / Modal / ui / Modal.props.ts

import { ReactNode } from 'react';  
  
export interface IModalProps {  
    /** триггер открытия модалки */  
    label: ReactNode;  
    /** контент модалки */  
    content: ReactNode;  
    /** начальное состояние модалки */  
    isOpened?: boolean;  
}

Само модальное окно уже работает за счёт хука модалки. Тут остаётся только проанимировать открытие и закрытие модалки через сторонние библиотеки для анимации компонентов

src / shared / ui / Modal / ui / Modal.tsx

import React from 'react';  
import { useModal } from '@/shared/lib';  
import { Card } from '../../Card';  
import { Portal } from '../../Portal';  
import styles from './Modal.module.scss';  
import { IModalProps } from './Modal.props';  
  
export const Modal = ({ content, label, isOpened = false }: IModalProps) => {  
    const { modalRef, modalExpanded, handleOpenModal, stopPropagation } = useModal({ isOpened });  
  
    return (  
       <div ref={modalRef} className={styles.modal}>  
          <div onClick={handleOpenModal}>  
             <span className={styles.modal__label}>{label}</span>  
             {modalExpanded && (  
                <Portal>  
                   <div onClick={handleOpenModal} className={styles.modal__overlay}>  
                      <Card onClick={stopPropagation} className={styles.modal__content}>  
                         {content}  
                      </Card>  
                   </div>  
                </Portal>  
             )}  
          </div>  
       </div>  
    );  
};

30 Redux-toolkit. Entity. Тесты на всех уровнях МЕТКА

Далее нужно будет обустроить в проекте редакс.

Первым делом нужно будет проинстациировать начальную точку для нашей апишки в виде RTK Query

src / shared / api / common.api.ts

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { USER_LOCAL_STORAGE_KEY } from '@/shared/const';
 
export const rtkApi = createApi({
	reducerPath: 'api',
	baseQuery: fetchBaseQuery({
		baseUrl: __API__,
		prepareHeaders: (headers: Headers /* api */): Headers => {
			const token = localStorage.getItem(USER_LOCAL_STORAGE_KEY);
 
			if (token) {
				headers.set('Authorization', token);
			}
 
			return headers;
		},
	}),
	endpoints: (builder) => ({}),
});

Далее нам нужно прописать типы тех данных, которые мы будем получать с сервера.

Конкретно нужно создать сущность User в entity, в которой нужно будет полностью описать модель взаимодействия с пользователем.

src / entities / User / model / types / user.interface.ts

import { IFeatures } from '@/shared/lib';  
import { IJsonSettings } from './userSettings.interface';  
  
/** список пользователей */  
export interface IUserList {  
    users: IUser[];  
}  
  
/** интерфейс пользователя */  
export interface IUser {  
    id: string;  
    username: string;  
    password: string;  
    roles: string[];  
    features: IFeatures;  
    avatar: string;  
    jsonSettings?: IJsonSettings;  
}

Заранее можно будет прописать типы ролей пользователей

src / entities / User / model / const / userRole.const.ts

export enum EUserRole {  
    ADMIN = 'admin',  
    MANAGER = 'manager',  
    USER = 'user',  
}

Так же для типизиации стора нужно будет написать схему пользователя. Схема будет из себя пока представлять просто тип пользователя с необязательными полями. Поля необязательны, так как пользователь может прийти незарегистрированный.

src / entities / User / model / types / user.schema.ts

import { IUser } from './user.interface';  
  
export interface UserSchema {  
    user?: IUser;  
}

Далее нам нужно будет реализовать схему, которая будет описывать полностью все наши данные в стейте редакса. Это нужно, чтобы в селекторах нормально получать существующие данные.

src / shared / lib / types / state.schema.ts

import { UserSchema } from '@/entities/User';  
import { rtkApi } from '@/shared/api';  
  
export interface StateSchema {  
    user: UserSchema;  
    [rtkApi.reducerPath]: ReturnType<typeof rtkApi.reducer>;  
}  
  
export interface ThunkExtraArg {  
    api: object;  
}

Далее эту схему нужно будет передать и в инишл стейт слайса

src / entities / User / model / slice / user.slice.ts

import { createSlice, PayloadAction } from '@reduxjs/toolkit';  
import { IUser, UserSchema } from '../types';  
  
const initialState: UserSchema = {};  
  
const userSlice = createSlice({  
    name: 'user',  
    initialState,  
    reducers: {  
       setUser: (state: UserSchema, action: PayloadAction<IUser>) => {  
          state.user = action.payload;  
       },  
    },  
});  
  
export const { actions: userActions } = userSlice;  
export const { reducer: userReducers } = userSlice;

Далее таким образом уже будут выглядеть тесты слайса пользователя:

src / entities / User / model / slice / user.slice.test.ts

import { IUser, userActions, userReducers } from '@/entities/User';  
import { UserSchema } from '../types';  
  
describe('User Slice Suite', () => {  
    test('set user into state', () => {  
       const state: UserSchema = {  
          user: {  
             id: '12231231',  
             avatar: 'asdasd',  
             features: {  
                isAppRedesigned: false,  
                isCounterEnabled: false,  
                isArticleRatingEnabled: false,  
             },  
             password: 'asdasd',  
             username: 'Alex',  
             roles: ['admin'],  
          },  
       };  
  
       expect(userReducers(state, userActions.setUser(state.user as IUser))).toEqual({  
          user: state.user,  
       });  
    });  
  
    test('set user undefined', () => {  
       const state: UserSchema = {  
          user: undefined,  
       };  
  
       expect(userReducers(undefined, userActions.setUser(state.user as IUser))).toEqual({  
          user: undefined,  
       });  
    });  
});

Далее нужно будет описать типы для получения пользователя в RTK Query

src / entities / User / model / types / user.service.interface.ts

import { IUser } from './user.interface';  
  
export interface IGetUserRequest {  
    id: string | number;  
}  
export interface IGetUserResponse extends IUser {}

И далее нужно будет заинжектить через injectEndpoints ендпоинты в корневой rtkApi

src / entities / User / api / user.api.ts

import { rtkApi } from '@/shared/api';  
import { IGetUserRequest, IGetUserResponse } from '../model/types';  
  
const userApi = rtkApi.injectEndpoints({  
    endpoints: ({ query, mutation }) => ({  
       getUser: query<IGetUserResponse, IGetUserRequest>({  
          query: ({ id }) => ({  
             url: '/user/' + id,  
          }),  
       }),  
    }),  
});  
  
export const { useGetUserQuery } = userApi;

И далее в конфиге провайдера нужно будет реализовать функцию, которая будет собирать стейт из различных асинхронных редьюсеров и начального состояния.

Стейт полностью протипизирован и реализован минимальный конфиг для devTools

src / app / providers / StoreProvider / config / store.ts

import {  
    Action,  
    AnyAction,  
    configureStore,  
    MiddlewareArray,  
    ReducersMapObject,  
    ThunkMiddleware,  
} from '@reduxjs/toolkit';  
import { ToolkitStore } from '@reduxjs/toolkit/dist/configureStore';  
import { CurriedGetDefaultMiddleware } from '@reduxjs/toolkit/dist/getDefaultMiddleware';  
// import { Reducer, CombinedState } from 'redux';  
import { userReducers } from '@/entities/User';  
import { rtkApi } from '@/shared/api';  
import { StateSchema, ThunkExtraArg } from '@/shared/lib';  
  
/**  
 * Функция сборки редакс-стейта 
 * */
export const createReduxStore = (  
    initialState?: StateSchema,  
    asyncReducers?: ReducersMapObject<StateSchema>,  
) => {  
    const reducers: ReducersMapObject<StateSchema> = {  
       ...asyncReducers,  
       user: userReducers,  
       [rtkApi.reducerPath]: rtkApi.reducer,  
    };  
  
    const extraArgument: ThunkExtraArg = { api: {} };  
  
    const store: ToolkitStore<  
       StateSchema,  
       Action<unknown>,  
       MiddlewareArray<[ThunkMiddleware<StateSchema, AnyAction, ThunkExtraArg>]>  
    > = configureStore({  
       reducer: reducers, // as Reducer<CombinedState<StateSchema>>,  
       preloadedState: initialState,  
       devTools: __IS_DEV__ ? { shouldHotReload: true } : false,  
       middleware: (getDefaultMiddleware: CurriedGetDefaultMiddleware<StateSchema>) =>  
          getDefaultMiddleware({ thunk: { extraArgument } }),  
    });  
  
    return store;  
};  
  
export type TRootState = ReturnType<typeof createReduxStore>['getState'];  
export type TAppDispatch = ReturnType<typeof createReduxStore>['dispatch'];

После организации стора нужно реализовать хуки для диспетча и получения данных

src / shared / lib / hooks / useReduxValue.tsx

import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';  
import { TAppDispatch, TRootState } from '@/app/providers/StoreProvider';  
  
/** хук диспетча */  
export const useAppDispatch = () => useDispatch<TAppDispatch>();  
  
/** хук селектора */  
export const useAppSelector: TypedUseSelectorHook<TRootState> = useSelector;  
  
/** получение значения из рекдакса */  
export const useReduxValue = () => {};

И теперь мы можем реализовать провайдер для стора

src / app / providers / StoreProvider / ui / StoreProvider.tsx

import { ReducersMapObject } from '@reduxjs/toolkit';  
import { FC, ReactNode } from 'react';  
import { Provider } from 'react-redux';  
import { StateSchema } from '@/shared/lib';  
import { createReduxStore } from '../config/store';  
  
interface IStoreProviderProps {  
    children: ReactNode;  
    initialState?: DeepPartial<StateSchema>;  
    asyncReducers?: DeepPartial<ReducersMapObject<StateSchema>>;  
}  
  
export const StoreProvider: FC<IStoreProviderProps> = ({  
    children,  
    initialState,  
    asyncReducers,  
}: IStoreProviderProps) => {  
    const store = createReduxStore(  
       initialState as StateSchema,  
       asyncReducers as ReducersMapObject<StateSchema>,  
    );  
  
    return <Provider store={store}>{children}</Provider>;  
};

Далее нужно поговорить про библиотеку reselect, которая уже встроена в RTK. Конкретно функция createSelector позволяет нам получать сразу несколько значений из стора и производить над ними определённые операции в конечном коллбэке.

Так же эта функция занимается мемоизацией полученного значения like useMemo

const selectTodosByCategory = createSelector(
  [
    // прокидываем массив функций-селекторов
    (state: RootState) => state.todos,
    (state: RootState, category: string) => category
  ],
  // получаем из них значения и производим над ними операции
  (todos, category) => {
    return todos.filter(t => t.category === category)
  }
)

И тут уже можно будет воспользоваться функцией createSelector которая выполнит нужную логику в селекторе и замемоизирует значение

src / entities / User / model / selectors / getUserRole.selector.ts

import { createSelector } from '@reduxjs/toolkit';  
import { EUserRole } from '@/entities/User';  
import { StateSchema } from '@/shared/lib';  
  
export const getUserRole = (state: StateSchema) => state.user.user?.roles;  
  
export const isUserAdmin = createSelector(  
    getUserRole,  
    (roles) => !!roles?.includes(EUserRole.ADMIN),  
);  
  
export const isUserManager = createSelector(  
    getUserRole,  
    (roles) => !!roles?.includes(EUserRole.MANAGER),  
);

А уже так будут выглядеть тесты селекторов:

src / entities / User / model / selectors / getUserRole / getUserRole.selector.test.ts

import { getUserRole, isUserAdmin, isUserManager } from '@/entities/User';  
import { StateSchema } from '@/shared/lib';  
  
describe('getUserRole', () => {  
    test('is user', () => {  
       const state: DeepPartial<StateSchema> = {  
          user: {  
             user: {  
                roles: ['user'],  
             },  
          },  
       };  
  
       expect(getUserRole(state as StateSchema)).toEqual(['user']);  
    });  
  
    test('is manager', () => {  
       const state: DeepPartial<StateSchema> = {  
          user: {  
             user: {  
                roles: ['manager'],  
             },  
          },  
       };  
  
       expect(isUserManager(state as StateSchema)).toEqual(true);  
    });  
  
    test('is admin', () => {  
       const state: DeepPartial<StateSchema> = {  
          user: {  
             user: {  
                roles: ['admin'],  
             },  
          },  
       };  
  
       expect(isUserAdmin(state as StateSchema)).toEqual(true);  
    });  
});

Далее добавляем провайдер стора в приложение

src / index.tsx

import React, { StrictMode, Suspense } from 'react';  
import { createRoot, Root } from 'react-dom/client';  
import { BrowserRouter } from 'react-router-dom';  
import App from '@/app/App';  
import { ErrorBoundary } from '@/app/providers/ErrorBoundary';  
import { StoreProvider } from '@/app/providers/StoreProvider';  
import { ThemeProvider } from '@/app/providers/ThemeProvider';  
import { Skeleton } from '@/widgets/Skeleton';  
import '@/shared/config/i18n/i18n';  
  
const root: Root = createRoot(document.getElementById('root') as HTMLElement);  
  
if (!root) {  
    throw new Error('В приложение не вмонтирован root div !!');  
}  
  
root.render(  
    <StoreProvider>       
	    <BrowserRouter>  
          <ErrorBoundary>  
             <ThemeProvider>  
                <StrictMode>  
                   <Suspense fallback={<Skeleton />}>  
                      <App />  
                   </Suspense>  
                </StrictMode>  
             </ThemeProvider>  
          </ErrorBoundary>  
       </BrowserRouter>  
    </StoreProvider>,  
);

31.1 Json server. Имитация бэкенда

Устанавливаем зависимости для поднятия искуственного бэк-сервера

npm install --save-dev json-server nodemon ts-node @types/json-server @types/nodemon

Добавим немного моковых данных в наш сервер

server / db.json

"users": [  
    {  
       "id": "1",  
       "username": "admin",  
       "password": "123",  
       "roles": [  
          "ADMIN"  
       ],  
       "features": {  
          "isArticleRatingEnabled": true,  
          "isCounterEnabled": true,  
          "isAppRedesigned": true  
       },  
       "avatar": "https://mobimg.b-cdn.net/v3/fetch/22/2207633df03a819cd72889249c8361a8.jpeg?w=1470&r=0.5625",  
       "jsonSettings": {  
          "isArticlesPageWasOpened": true,  
          "theme": "app_dark_theme"  
       }  
    },
...

Далее нам нужно будет сгенерировать пару сертификатов, чтобы у нас была возможность поднять https-бэк

|500

Настройка json-server, которая позволит нам имитировать настоящий сервер

server / index.ts

import fs from 'fs';  
import http from 'http';  
import https from 'https';  
import path from 'path';  
import jsonServer from 'json-server';  
import { IUser } from './types/user.interface';  
  
/**  
 * Сертификаты для https-сервера * 
 * */
const options = {  
    key: fs.readFileSync(path.resolve(__dirname, 'key.pem')),  
    cert: fs.readFileSync(path.resolve(__dirname, 'cert.pem')),  
};  
  
/**  
 * инстанциируем json-сервер * 
 * */
const server = jsonServer.create();  
  
/**  
 * инстанциируем роутер по файлу с данными
 * */
const router = jsonServer.router(path.resolve(__dirname, 'db.json'));  
  
server.use(jsonServer.defaults({}));  
server.use(jsonServer.bodyParser);  
  
/**  
 * Для имитации "реального" апи добавлена небольшая задержка для возврата данных 
 * */
server.use(async (req, res, next) => {  
    await new Promise((res) => {  
       setTimeout(res, 800);  
    });  
    next();  
});  
  
/**  
 * эндпоинт авторизации пользователя * 
 * */
server.post('/login', (req, res) => {  
    try {  
       const { username, password } = req.body;  
       const db = JSON.parse(  
          fs.readFileSync(path.resolve(__dirname, 'db.json'), { encoding: 'utf-8' }),  
       );  
       const { users = [] } = db;  
  
       const userFromBd = users.find(  
          (user: IUser) => user.username === username && user.password === password,  
       );  
  
       if (userFromBd) {  
          return res.json(userFromBd);  
       }  
  
       return res.status(403).json({ message: 'User not found' });  
    } catch (e) {  
       if (e instanceof Error) {  
          console.log(e);  
          return res.status(500).json({ message: e.message });  
       }  
    }  
});  
  
/**  
 * гуард авторизации пользователя * просто проверяет заголовки авторизации пользователя в запросе
 * */
server.use((req, res, next) => {  
    if (!req.headers.authorization) {  
       return res.status(403).json({ message: 'AUTH ERROR' });  
    }  
  
    next();  
});  
  
server.use(router);  
  
/**  
 * старт сервера
 */
const HTTPS_PORT = 8443;  
const HTTP_PORT = 8000;  
  
const httpsServer = https.createServer(options, server);  
const httpServer = http.createServer(server);  
  
httpsServer.listen(HTTPS_PORT, () => {  
    console.log(`https server is running on ${HTTPS_PORT} port`);  
});  
  
httpServer.listen(HTTP_PORT, () => {  
    console.log(`http server is running on ${HTTP_PORT} port`);  
});

Минимальный список интерфейсов, которые нужно описать для пользователя

server / types / user.interface.ts

/** список пользователей */  
export interface IUserList {  
    users: IUser[];  
}  
  
/** интерфейс пользователя */  
export interface IUser {  
    id: string;  
    username: string;  
    password: string;  
    roles: string[];  
    features: IFeatures;  
    avatar: string;  
    jsonSettings?: IJsonSettings;  
}  
  
/** список фич, которые активировал пользователь */  
export interface IFeatures {  
    isArticleRatingEnabled: boolean;  
    isCounterEnabled: boolean;  
    isAppRedesigned?: boolean;  
}  
  
/** настройки пользователя */  
export interface IJsonSettings {  
    isArticlesPageWasOpened: boolean;  
    theme?: string;  
}

Команда поднятия сервера

package.json

"start:dev:server": "nodemon ./server/index.ts",

32 Кастомный Input. Окно авторизации. Lazy modal МЕТКА

Опишем типы для инпута

src / shared / ui / Input / ui / Input.props.ts

export interface IInputAttributes  
    extends DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> {}  
  
export interface InputProps extends Omit<IInputAttributes, 'onChange' | 'value'> {  
    autofocus?: boolean;  
    value?: string;  
    onChange?: (value: string) => void;  
}

Реализуем инпут, который сразу будет принимать на себя фокус. Так же, чтобы было проще вводить данные в него, сразу захэндлим инпут, чтобы тот передавал в onChange сразу значение из инпута

src / shared / ui / Input / ui / Input.tsx

import React, { ChangeEvent, MutableRefObject, useEffect, useId, useRef, useState } from 'react';  
import { cn } from '@/shared/lib';  
import { InputProps } from './Input.props';  
  
export const Input = ({ autofocus, onChange, value, type = 'text', ...props }: InputProps) => {  
    const id = useId();  
    const ref: MutableRefObject<HTMLInputElement | null> = useRef<HTMLInputElement>(null);  
    const [focus, setFocus] = useState<boolean>(false);  
  
    /** стили при активном инпуте */  
    const onFocus = () => setFocus(true);  
    /** стили при выходе из инпута */  
    const onBlur = () => setFocus(false);  
  
    const handleOnChange = (event: ChangeEvent<HTMLInputElement>) => {  
       onChange?.(event.target.value);  
    };  
  
    useEffect(() => {  
       if (autofocus) {  
          setFocus(true);  
          ref.current?.focus();  
       }  
    }, [autofocus]);  
  
    return (  
       <label htmlFor={id} className={cn()}>  
          <input  
             ref={ref}  
             id={id}  
             type={type}  
             onChange={handleOnChange}  
             value={value}  
             onFocus={onFocus}  
             onBlur={onBlur}  
             {...props}  
          />  
       </label>  
    );  
};

Далее нужно описать схему для окна авторизации

src / features / BaseAuth / model / types / schema / baseAuth.schema.ts

export interface BaseAuthSchema {  
    username?: string;  
    password?: string;  
}

Создаём слайс для ввода нового значения в состояние

src / features / BaseAuth / model / slice / baseAuth.slice.ts

import { createSlice, PayloadAction } from '@reduxjs/toolkit';  
import { BaseAuthSchema } from '../types/schema/baseAuth.schema';  
  
const initialState: BaseAuthSchema = {};  
  
const baseAuthSlice = createSlice({  
    name: 'baseAuth',  
    initialState,  
    reducers: {  
       setPassword: (state: BaseAuthSchema, action: PayloadAction<string>) => {  
          state.password = action.payload;  
       },  
       setUsername: (state: BaseAuthSchema, action: PayloadAction<string>) => {  
          state.username = action.payload;  
       },  
    },  
});  
  
export const { reducer: baseAuthReducer } = baseAuthSlice;  
export const { actions: baseAuthActions } = baseAuthSlice;

Далее нужно реализовать селектор логина

src / features / BaseAuth / model / selectors / getLogin / getLogin.selector.ts

import { StateSchema } from '@/shared/types';  
  
export const getLogin = (state: StateSchema) => state.baseAuth?.username || '';

И пароля

src / features / BaseAuth / model / selectors / getPassword / getPassword.selector.ts

import { StateSchema } from '@/shared/types';  
  
export const getPassword = (state: StateSchema) => state?.baseAuth?.password || '';

И таким образом будет реализована передача данных через инпуты

src / features / BaseAuth / ui / BaseAuth.tsx

import React, { useCallback } from 'react';  
import { useTranslation } from 'react-i18next';  
import { DynamicModuleLoader, ReducerList, useAppDispatch, useAppSelector } from '@/shared/lib';  
import { Input } from '@/shared/ui';  
import { getPassword, getLogin } from '../model/selectors';  
import { baseAuthReducer, baseAuthActions } from '../model/slice/baseAuth.slice';  
  
const initialReducers: ReducerList = {  
    baseAuth: baseAuthReducer,  
};  
  
export const BaseAuth = () => {  
    const { t } = useTranslation();  
    const dispatch = useAppDispatch();  
    const login = useAppSelector(getLogin);  
    const password = useAppSelector(getPassword);  
  
    const loginOnChange = useCallback(  
       (value: string) => dispatch(baseAuthActions.setUsername(value)),  
       [dispatch],  
    );  
    const passwordOnChange = useCallback(  
       (value: string) => dispatch(baseAuthActions.setPassword(value)),  
       [dispatch],  
    );  
  
    return (  
       <DynamicModuleLoader removeAfterUnmount reducers={initialReducers}>  
          <div>{t('Auth')}</div>  
          <Input placeholder={'Логин'} onChange={loginOnChange} value={login} />  
          <Input placeholder={'Пароль'} onChange={passwordOnChange} value={password} />  
       </DynamicModuleLoader>  
    );  
};

33 Husky. Pre commit хуки

Плагин husky будет за нас выполнять какие-либо простые операции до того, как мы зальём изменения в ветку.

Для начала нужно будет проинициализировать хаски:

npx husky-init

Далее нужно будет добавить наши npm-скрипты

.husky / pre-commit

#!/usr/bin/env sh  
. "$(dirname -- "$0")/_/husky.sh"  
  
npm run lint:fix:all  
npm run test:unit  
npm run storybook:build  
npm run test:ui:ci

34 Авторизация. Reducers, slices, async thunk. Custom text МЕТКА