Un use case est une brique métier, indépendante de toute interface utilisateur et service externe. Les use case sont construit avec Redux toolkit. Commencez par lire la documentation de redux toolkit avant de continuer.
/use-cases/authentication
└── index.ts
└── authentication.adapters.ts
└── authentication.selectors.ts
└── authentication.slice.ts
└── authentication.saga.ts
└── authentication.spec.ts
index.ts
- api public du use-caseauthentication.adapters.ts
- utilistaire pour les requêtes et les entitiésauthentication.selectors.ts
- selecteurs reduxauthentication.slice.ts
- slice redux - documentation officielleauthentication.saga.ts
- saga redux - documentation officielleauthentication.spec.ts
- test unitaires
index.ts
import { saga } from './authentication.saga';
import { slice } from './authentication.slice';
export * from './authentication.selectors';
export const authenticationActions = slice.actions;
export const authenticationConfig = {
slice,
saga,
};
Un slice est une combinaison de reducer + actions. Voir Redux toolkit slice documentation
authentication.slice.ts
import { SliceRootState } from 'src/store/utils';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface State {
userId: string | null;
isFetching: boolean;
}
const initialState: State = {
userId: null,
isFetching: false,
};
const slice = createSlice({
name: 'authentication',
initialState,
reducers: {
fetchCurrentUserRequested(state) {
state.isFetching = true;
},
fetchCurrentUserSucceeded(state, action: PayloadAction<{ user: User }>) {
state.user = action.payload.user;
},
},
});
export type RootState = SliceRootState<typeof slice>;
Un selecteur permet d'exporter les données du store de manière public.
authentication.selectors.ts
import { RootState } from './authentication.slice.ts';
export function selectCurrentUser(state: RootState) {
return state.authentication.user;
}
official redux saga documentation
Redux saga permet de créer des effet de bord comme des appels api, mais également de gérer des scénario complexes.
Par défaut, redux saga fonctionne assez mal avec TypeScript. Pour pallier à ce problème, nous utilisons typed-redux-saga, une surcouche qui permet d'améliorer le typage des effect comme call
, select
, etc.
authentication.saga.ts
import { takeLatest, call } from 'typed-redux-saga';
import { authenticationSlice } from './authentication.slice';
const { fetchCurrentUserRequested, fetchCurrentUserSucceeded } = slice.actions;
function* fetchCurrentUserRequestedSaga(action: ReturnType<typeof fetchCurrentUserRequested>) {
const user = yield* call(...);
yield put(fetchCurrentUserSucceeded({ user }));
}
export function* authenticationSaga() {
yield* takeLatest(fetchCurrentUserRequested, fetchCurrentUserRequestedSaga);
}
createRequestAdapter()
est un utilitaire qui permet de générer des actions et selecteurs pour gérer des requêtes api.
actions:
requested
succeeded
failed
reset
authentication.adapters.ts
import {
createRequestAdapter,
RequestState,
SliceRootState,
} from 'src/store/utils';
export const loginRequestAdapter = createRequestAdapter('login').withPayloads<
{ email: string; password: string }, // request payload
{ accessToken: string }, // success payload
void // failed payload
>();
authentication.slice.ts
interface State {
accessToken: string | null;
login: RequestState<typeof loginRequestAdapter>
}
const initialState: State {
accessToken: null,
login: loginRequestAdapter.getInitialState()
}
export const slice = createSlice({
name: 'authentication',
initialState,
reducers: {
...loginRequestAdapter.getReducers<State>(state => state.login, {
loginSucceeded(state, action) {
state.accessToken = action.payload.accessToken;
},
}),
},
});
...
authentication.selector.ts
import { RootState, loginRequest } from './authentication.slice';
export const loginSelectors = loginRequest.getSelectors<RootState>(
(state) => state.authentication.login
);
createEntityAdapter()
est un utilitaire fourni par redux toolkit et qui permet de générer des entitée (création, modification, suppression). Voir redux-toolkit createEntityAdapter()
Exemple:
candidats.adapters.ts
import { createEntityAdapter, EntityState } from '@reduxjs/toolkit';
import { SliceRootState } from 'src/store/utils';
import { Candidat } from 'src/api/types';
export const candidatEntityAdapter = createEntityAdapter<Candidat>();
candidats.slice.ts
import { EntityState } from '@reduxjs/toolkit';
import { Candidat } from 'src/api/types';
interface State {
candidats: EntityState<Candidat>;
}
const initialState: State {
candidats: candidatEntityAdapter.getInitialState(),
}
export const slice = createSlice({
name: 'candidats',
initialState,
reducers: {
fetchCandidatsSucceeded(state, action: Payload<{ candidats: Candidat[] }>) {
candidatEntityAdapter.setAll(state.candidats, action.payload.candidats);
}
},
});
...
candidats.selectors.ts
import { RootState } from './candidats.slice';
import { candidatEntityAdapter } from './candidats.adapters';
export const { selectAll: selectCandidats } =
candidatEntityAdapter.getSelectors<RootState>(
(state) => state.candidats.candidats
);
src/use-cases/index.ts
il suffit d'ajouter la configuration du nouveau use case dans l'objet useCaseConfig
import { authenticationConfig } from 'use-cases/authentication';
export const useCaseConfig = {
authenticationConfig,
};
DashboardPage.tsx
import { useSelector, useDispatch } from 'react-redux';
import {
selectCurrentUser,
selectIsFetchUserRequested,
authenticationActions,
} from './use-cases/authentication';
export function DashboardPage() {
const dispatch = useDispatch();
const isFetchUserRequested = useSelector(selectIsFetchUserRequested);
const currentUser = useSelector(selectCurrentUser);
useEffect(() => {
dispatch(authenticationActions.fetchUserRequested());
}, [dispatch]);
if (isFetchUserRequested) {
return <Loader />;
}
return <div>{user.name}</div>;
}