Redux-classic
yarn add redux react-redux redux-thunk @types/react-redux redux-devtools-extension
store/index.ts
import { applyMiddleware, legacy_createStore } from 'redux';
import thunk from 'redux-thunk';
import { RootState, rootReducer } from './rootReducer';
import { TypedUseSelectorHook, useSelector } from 'react-redux';
import { composeWithDevTools } from 'redux-devtools-extension';
export const testStore = legacy_createStore(
rootReducer,
composeWithDevTools(applyMiddleware(thunk)),
);
// экспорт хука, типизированного селектора всего стейта приложения
export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;
store/rootReducer.ts
import { combineReducers } from 'redux';
// reducers list
import { appReducer } from './app/app.reducer';
import { loginReducer } from './login/login.reducer';
export const rootReducer = combineReducers({
app: appReducer,
login: loginReducer,
});
// импорт типизации всего state
export type RootState = ReturnType<typeof rootReducer>;
store/actions.ts
import { useDispatch } from 'react-redux';
import * as appActionCreators from './app/app.actionCreators';
import * as loginActionCreators from './login/login.actionCreators';
import { bindActionCreators } from 'redux';
// import * as loginActionCreators from './login/login.actionCreators';
// экспорт хука, в котором находятся все экшены приложения
export const useActions = () => {
const dispatch = useDispatch();
return bindActionCreators(
{
...appActionCreators,
...loginActionCreators,
},
dispatch,
);
};
store/app/app.reducer.ts
import {
TAppState,
TAppActions,
AppActionTypes,
AppScreens,
} from './app.types';
const appInitialState: TAppState = {
screen: AppScreens.LOGIN,
alertMessage: {
message: '',
status: undefined,
},
userData: {
token: undefined,
expiresIn: undefined,
},
};
export const appReducer = (
state: TAppState = appInitialState,
action: TAppActions,
): TAppState => {
switch (action.type) {
// SET_SCREEN
case AppActionTypes.SET_SCREEN:
return { ...state, screen: action.payload };
// SAVE_USER_DATA
case AppActionTypes.SAVE_USER_DATA:
return { ...state, userData: action.payload };
// SET_ALERT_MESSAGE
case AppActionTypes.SET_ALERT_MESSAGE:
return { ...state, alertMessage: action.payload };
// DELETE_ALERT_MESSAGE
case AppActionTypes.DELETE_ALERT_MESSAGE:
return {
...state,
alertMessage: {
message: '',
status: undefined,
},
};
// DEFAULT
default:
return state;
}
};
store/app/app.types.ts
import { Statuses } from '@/types/common';
// screens enum
export enum AppScreens {
LOGIN = 'LOGIN',
DASHBOARD = 'DASHBOARD',
EDIT_DRUGSTORE = 'EDIT_DRUGSTORE',
EDIT_MULTI_DRUGSTORE = 'EDIT_MULTI_DRUGSTORE',
CUSTOM_STATUSES = 'CUSTOM_STATUSES',
}
// action types enum
export enum AppActionTypes {
SET_SCREEN = 'SET_SCREEN',
SAVE_USER_DATA = 'SAVE_USER_DATA',
SET_ALERT_MESSAGE = 'SET_ALERT_MESSAGE',
DELETE_ALERT_MESSAGE = 'DELETE_ALERT_MESSAGE',
}
export type TUserData = {
token?: string;
expiresIn?: number;
};
export type TAlertMessage = {
message: string;
status?: Statuses;
};
// типизация стейта
export type TAppState = {
screen: AppScreens;
alertMessage: TAlertMessage;
userData: TUserData;
};
// типизация экшенов
export type TSetScreenAction = {
type: AppActionTypes.SET_SCREEN;
payload: AppScreens;
};
export type TSaveUserDataAction = {
type: AppActionTypes.SAVE_USER_DATA;
payload: TUserData;
};
export type TSetAlertMessageAction = {
type: AppActionTypes.SET_ALERT_MESSAGE;
payload: TAlertMessage;
};
export type TDeleteAlertMessageAction = {
type: AppActionTypes.DELETE_ALERT_MESSAGE;
};
// экспорт всех типов экшенов
export type TAppActions =
| TSetScreenAction
| TSaveUserDataAction
| TSetAlertMessageAction
| TDeleteAlertMessageAction;
store/app/app.actionCreators.ts
import {
AppActionTypes,
AppScreens,
TAlertMessage,
TDeleteAlertMessageAction,
TSaveUserDataAction,
TSetAlertMessageAction,
TSetScreenAction,
TUserData,
} from './app.types';
// setScreen
export const setScreen = (payload: AppScreens): TSetScreenAction => ({
type: AppActionTypes.SET_SCREEN,
payload,
});
// saveUserData
export const saveUserData = (payload: TUserData): TSaveUserDataAction => ({
type: AppActionTypes.SAVE_USER_DATA,
payload,
});
// setAlertMessage
export const setAlertMessage = (
payload: TAlertMessage,
): TSetAlertMessageAction => ({
type: AppActionTypes.SET_ALERT_MESSAGE,
payload,
});
// deleteAlertMessage
export const deleteAlertMessage = (): TDeleteAlertMessageAction => ({
type: AppActionTypes.DELETE_ALERT_MESSAGE,
});
store/login/login.reducer.ts
import { TLoginState, TLoginActions, LoginActionTypes } from './login.types';
const loginInitialState: TLoginState = {
isLoginLoading: false,
isLoginSuccess: false,
isLoginError: false,
};
export const loginReducer = (
state: TLoginState = loginInitialState,
action: TLoginActions,
): TLoginState => {
switch (action.type) {
// LOGIN_LOADING
case LoginActionTypes.LOGIN_LOADING:
return { ...state, isLoginLoading: action.payload };
// LOGIN_SUCCESS
case LoginActionTypes.LOGIN_SUCCESS:
return { ...state, isLoginSuccess: true };
// LOGIN_ERROR
case LoginActionTypes.LOGIN_ERROR:
return { ...state, isLoginError: true };
// LOGIN_RESET
case LoginActionTypes.LOGIN_RESET:
return loginInitialState;
// DEFAULT
default:
return state;
}
};
store/login/login.types.ts
// action types enum
export enum LoginActionTypes {
LOGIN_LOADING = 'LOGIN_LOADING',
LOGIN_SUCCESS = 'LOGIN_SUCCESS',
LOGIN_ERROR = 'LOGIN_ERROR',
LOGIN_RESET = 'LOGIN_RESET',
}
// типизация стейта
export type TLoginState = {
isLoginLoading: boolean;
isLoginSuccess: boolean;
isLoginError: boolean;
};
// типизация экшенов
export type TLoginLoadingAction = {
type: LoginActionTypes.LOGIN_LOADING;
payload: boolean;
};
export type TLoginSuccessAction = {
type: LoginActionTypes.LOGIN_SUCCESS;
};
export type TLoginErrorAction = {
type: LoginActionTypes.LOGIN_ERROR;
};
export type TLoginResetAction = {
type: LoginActionTypes.LOGIN_RESET;
};
// экспорт всех типов экшенов
export type TLoginActions =
| TLoginLoadingAction
| TLoginSuccessAction
| TLoginErrorAction
| TLoginResetAction;
store/login/login.actionCreators.ts
import Cookies from 'js-cookie';
import { Dispatch } from 'redux';
import {
LoginActionTypes,
TLoginActions,
TLoginErrorAction,
TLoginLoadingAction,
TLoginResetAction,
TLoginSuccessAction,
} from './login.types';
import {
TLoginValues,
loginService,
// loginService2,
} from '../../api/services/login-sevice';
import {
saveUserData,
setAlertMessage,
setScreen,
} from '../app/app.actionCreators';
import {
AppScreens,
TSaveUserDataAction,
TSetAlertMessageAction,
TSetScreenAction,
} from '../app/app.types';
import { Statuses } from '@/types/common';
// loginLoading
export const loginLoading = (payload: boolean): TLoginLoadingAction => ({
type: LoginActionTypes.LOGIN_LOADING,
payload,
});
// loginSuccess
export const loginSuccess = (): TLoginSuccessAction => ({
type: LoginActionTypes.LOGIN_SUCCESS,
});
// loginError
export const loginError = (): TLoginErrorAction => ({
type: LoginActionTypes.LOGIN_ERROR,
});
// loginReset
export const loginReset = (): TLoginResetAction => ({
type: LoginActionTypes.LOGIN_RESET,
});
// loginThunk - THEN-CATCH variant
export function loginThunk(loginData: TLoginValues) {
return (
// all types of actions inside
dispatch: Dispatch<
| TLoginActions
| TSetAlertMessageAction
| TSetScreenAction
| TSaveUserDataAction
>,
) => {
// loading
dispatch(loginLoading(true));
loginService
// LOGIN REQUEST
.logIn(loginData)
// ========= LOGIN SUCCESS ========= //
.then(res => {
dispatch(loginSuccess());
// save userData to store
dispatch(saveUserData(res.data));
// save userData to localStorage
localStorage.setItem(
'pickup-points-userdata',
JSON.stringify(res.data),
);
// save userData to cookies
Cookies.set('pickup-points-userdata', JSON.stringify(res.data));
// success alert
dispatch(
setAlertMessage({
message: `Login success`,
status: Statuses.success,
}),
);
// redirect after 1500
setTimeout(() => {
dispatch(loginLoading(false));
dispatch(setScreen(AppScreens.DASHBOARD));
}, 1500);
})
// ========= LOGIN ERROR ========= //
.catch(error => {
dispatch(loginError());
dispatch(loginLoading(false));
// error alert
dispatch(
setAlertMessage({ message: `${error}`, status: Statuses.danger }),
);
});
};
}
// loginThunk2 - ASYNC-AWAIT variant
export function loginThunk2(loginData: TLoginValues) {
return async (
dispatch: Dispatch<
| TLoginActions
| TSetAlertMessageAction
| TSetScreenAction
| TSaveUserDataAction
>,
) => {
dispatch(loginLoading(true));
// login reauest
const response = await loginService.logIn(loginData);
// ========= LOGIN SUCCESS ========= //
if (response.status === 200) {
const responseSuccessData = response.data;
// save userData to store
dispatch(saveUserData(responseSuccessData));
// save userData to localStorage
localStorage.setItem(
'pickup-points-userdata',
JSON.stringify(responseSuccessData),
);
// save userData to cookies
Cookies.set(
'pickup-points-userdata',
JSON.stringify(responseSuccessData),
);
// success alert
dispatch(
setAlertMessage({
message: `Login success`,
status: Statuses.success,
}),
);
// redirect after 1500
setTimeout(() => {
dispatch(loginLoading(false));
dispatch(setScreen(AppScreens.DASHBOARD));
}, 1500);
// ========= LOGIN ERROR ========= //
} else {
dispatch(loginError());
dispatch(loginLoading(false));
// error alert
dispatch(
setAlertMessage({ message: `${response}`, status: Statuses.danger }),
);
// error data
// const responseErrorData = response.response.data;
}
};
}
httpClient
import { API_URL } from './apiUrl';
import axios from 'axios';
import { testStore } from '../store-redux-classic';
const httpClient = axios.create({
baseURL: API_URL,
headers: {
// inject userToken --> get token value from store
Authorization: testStore.getState().app.userData.token,
},
});
export default httpClient;
../loginService
import { AxiosResponse } from 'axios';
import httpClient from '../httpClient';
export type TLoginValues = {
username: string;
password: string;
};
// THEN-CATCH variant
export const loginService = {
// logIn
logIn(loginValues: TLoginValues): Promise<AxiosResponse> {
const { username, password } = loginValues;
return (
httpClient.post(`auth/login`, { username, password })
// login success
.then(res => {
// do smth
})
// logIn error
.catch(error => error)
);
},
};
How to use inside component
// import store hooks
import { useActions } from '../../../store-redux-classic/actions';
import { useTypedSelector } from '../../../store-redux-classic';
...
const { isLoginLoading } = useTypedSelector(state => state.login);
const { loginThunk } = useActions();
...
onSubmit: (values: TLoginFormValues) => {
const { username, password } = values;
loginThunk2({ username, password });
},