Configuration de Redux pour une utilisation dans une application du monde réel
Redux est une bibliothèque importante dans l'écosystème React, et presque la bibliothèque par défaut à utiliser lorsque vous travaillez sur des applications React qui impliquent une gestion d'état. En tant que tel, l'importance de savoir comment cela fonctionne ne peut être surestimée.
Ce guide guidera le lecteur dans la configuration de Redux dans une application React assez complexe et présentera au lecteur la configuration des «meilleures pratiques» en cours de route. Cela sera particulièrement bénéfique pour les débutants, et pour tous ceux qui souhaitent combler les lacunes dans leurs connaissances de Redux.
Présentation de Redux
Redux est une bibliothèque qui vise à résoudre le problème de la gestion des états dans les applications JavaScript en imposant des restrictions sur comment et quand les mises à jour d'état peuvent avoir lieu. Ces restrictions sont formées des «trois principes» de Redux qui sont:
Source unique de vérité
L'ensemble de l'étatde votre application
est conservé dans un magasin ReduxL'état est en lecture seule
La seule façon de changer l'état est d'envoyer les données sous la forme d'un objet simple, appelé action. Vous pouvez penser aux actions comme un moyen de dire à l'état: «J'ai des données que je voudrais insérer / mettre à jour / supprimer».Des modifications sont apportées avec des fonctions pures
Pour changer l'état de votre application, vous écrivez une fonction qui prend l'état précédent et une action et renvoie un nouvel objet d'état comme état suivant. Cette fonction est appelée un réducteur
Le dernier principe est le plus important dans Redux, et c'est là que le la magie de Redux se produit. Les fonctions de réduction ne doivent pas contenir de code imprévisible, ni effectuer d'effets secondaires tels que des requêtes réseau, et ne doivent pas muter directement l'objet d'état.
Redux est un excellent outil, comme nous l'apprendrons plus tard dans ce guide, mais il ne le fait pas. t venir sans ses défis ou ses compromis. Pour aider à rendre le processus d'écriture Redux efficace et plus agréable, l'équipe Redux propose une boîte à outils qui résume le processus de configuration d'un magasin Redux et fournit des modules complémentaires et des utilitaires Redux utiles qui aident à simplifier le code d'application. Par exemple, la bibliothèque utilise Immer.js une bibliothèque qui vous permet d'écrire sous le capot une logique de mise à jour immuable «mutative».
Lecture recommandée : [19659019] De meilleurs réducteurs avec Immer
Dans ce guide, nous explorerons Redux en créant une application qui permet aux utilisateurs authentifiés de créer et de gérer des agendas numériques.
Building Diaries.app
Comme indiqué dans Dans la section précédente, nous examinerons de plus près Redux en créant une application qui permet aux utilisateurs de créer et de gérer des agendas. Nous allons construire notre application en utilisant React, et nous allons configurer Mirage comme notre serveur de simulation d'API puisque nous n'aurons pas accès à un vrai serveur dans ce guide.
Démarrage d'un projet et installation de dépendances
Allons-y commencé notre projet. Tout d'abord, amorcez une nouvelle application React en utilisant create-react-app
:
En utilisant npx:
npx create-react-app diaries-app --template typescript
Nous commençons avec le modèle TypeScript, car nous pouvons améliorer notre expérience de développement en écrivant du code de type sécurisé.
Maintenant, installons les dépendances dont nous aurons besoin. Accédez au répertoire de votre projet nouvellement créé
cd diaries-app
Et exécutez les commandes suivantes:
npm install --save redux react-redux @ reduxjs / toolkit
npm install --save axios react-router-dom react-hook-form yup dayjs markdown-to-jsx sweetalert2
npm install --save-dev miragejs @ types / react-redux @ types / react-router-dom @ types / yup @ types / markdown-to-jsx
La première commande installera Redux, React-Redux (liaisons React officielles pour Redux) et la boîte à outils Redux.
La deuxième commande installe des packages supplémentaires qui seront utiles pour l'application que nous allons construire mais qui sont non requis pour travailler avec Redux.
La dernière commande installe Mirage et les déclarations de type pour les paquets que nous avons installés en tant que devDependencies.
Décrire l'état initial de l'application
Passons en revue les exigences de notre application en détail. L'application permettra aux utilisateurs authentifiés de créer ou de modifier des agendas existants. Les agendas sont privés par défaut, mais ils peuvent être rendus publics. Enfin, les entrées du journal seront triées en fonction de leur date de dernière modification.
Cette relation devrait ressembler à ceci:

Forts de ces informations, nous pouvons maintenant modéliser l'état de notre application. Tout d'abord, nous allons créer une interface pour chacune des ressources suivantes: User
Diary
et DiaryEntry
. Les interfaces dans Typescript décrivent la forme d'un objet.
Allez-y et créez un nouveau répertoire nommé interfaces
dans le sous-répertoire src
de votre application:
] interfaces cd src && mkdir
Ensuite, exécutez les commandes suivantes dans le répertoire que vous venez de créer:
touchez entry.interface.ts
touch diary.interface.ts
touchez user.interface.ts
Cela créera trois fichiers nommés entry.interface.ts diary.interface.ts et user.interface.ts respectivement. Je préfère conserver les interfaces qui seraient utilisées à plusieurs endroits dans mon application dans un même emplacement.
Ouvrez entry.interface.ts et ajoutez le code suivant pour configurer l'entrée
] interface:
interface d'exportation Entrée {
id?: chaîne;
titre: chaîne;
contenu: chaîne;
createdAt?: chaîne;
updatedAt?: chaîne;
diaryId?: chaîne;
}
Une entrée de journal typique aura un titre et du contenu, ainsi que des informations sur la date de sa création ou de sa dernière mise à jour. Nous reviendrons à la propriété diaryId
plus tard.
Ensuite, ajoutez ce qui suit à diary.interface.ts :
export interface Diary {
id?: chaîne;
titre: chaîne;
type: 'privé' | 'Publique';
createdAt?: chaîne;
updatedAt?: chaîne;
userId?: chaîne;
entryIds: chaîne [] | nul;
}
Ici, nous avons une propriété de type
qui attend une valeur exacte de «privé» ou de «public», car les agendas doivent être privés ou publics. Toute autre valeur générera une erreur dans le compilateur TypeScript.
Nous pouvons maintenant décrire notre objet User
dans le fichier user.interface.ts comme suit:
interface d'exportation Utilisateur {
id?: chaîne;
nom d'utilisateur: chaîne;
email: chaîne;
mot de passe?: chaîne;
diaryIds: chaîne [] | nul;
}
Une fois nos définitions de type terminées et prêtes à être utilisées dans notre application, configurons notre serveur d'API simulé à l'aide de Mirage.
Configuration de la simulation d'API avec MirageJS
Puisque ce didacticiel est axé sur Redux, nous n'irons pas dans les détails de la configuration et de l'utilisation de Mirage dans cette section. Veuillez consulter cette excellente série si vous souhaitez en savoir plus sur Mirage.
Pour commencer, accédez à votre répertoire src
et créez un fichier nommé serveur . ts
en exécutant les commandes suivantes:
mkdir -p services / mirage
services cd / mirage
# ~ / diaries-app / src / services / mirage
touch server.ts
Ensuite, ouvrez le fichier server.ts
et ajoutez le code suivant:
import {Server, Model, Factory, comesTo, hasMany, Response} from 'miragejs';
export const handleErrors = (error: any, message = 'Une erreur est survenue') => {
renvoie une nouvelle réponse (400, indéfini, {
Les données: {
message,
isError: vrai,
},
});
};
export const setupServer = (env?: chaîne): Serveur => {
retourner un nouveau serveur ({
environnement: env ?? 'développement',
des modèles: {
entrée: Model.extend ({
journal: appartient à (),
}),
journal: Model.extend ({
entrée: hasMany (),
utilisateur: appartient à (),
}),
utilisateur: Model.extend ({
journal: hasMany (),
}),
},
des usines: {
utilisateur: Factory.extend ({
nom d'utilisateur: 'test',
mot de passe: 'mot de passe',
email: 'test@email.com',
}),
},
graines: (serveur): any => {
server.create ('utilisateur');
},
routes (): void {
this.urlPrefix = 'https://diaries.app';
},
});
};
Dans ce fichier, nous exportons deux fonctions. Une fonction utilitaire pour gérer les erreurs et setupServer ()
qui renvoie une nouvelle instance de serveur. La fonction setupServer ()
prend un argument facultatif qui peut être utilisé pour modifier l'environnement du serveur. Vous pouvez l'utiliser pour configurer Mirage pour des tests ultérieurs.
Nous avons également défini trois modèles dans la propriété models du serveur: User
Diary
et Entrée
. Souvenez-vous qu'auparavant nous avons configuré l'interface Entry
avec une propriété nommée diaryId
. Cette valeur sera automatiquement définie sur l'ID
dans lequel l'entrée est enregistrée. Mirage utilise cette propriété pour établir une relation entre une Entrée
et un Agenda
. La même chose se produit également lorsqu'un utilisateur crée un nouveau journal: userId
est automatiquement défini sur l'ID de cet utilisateur.
Nous avons amorcé la base de données avec un utilisateur par défaut et configuré Mirage pour intercepter toutes les demandes de notre application à partir de avec https://diaries.app
. Notez que nous n’avons encore configuré aucun gestionnaire d’itinéraires . Allons-y et créons-en quelques-uns.
Assurez-vous que vous êtes dans le répertoire src / services / mirage puis créez un nouveau répertoire nommé routes à l'aide de la commande suivante:
] # ~ / diaries-app / src / services / mirage
routes mkdir
cd
dans le répertoire nouvellement créé et créez un fichier nommé user.ts :
cd routes
toucher user.ts
Ensuite, collez le code suivant dans le fichier user.ts
:
import {Response, Request} from 'miragejs';
import {handleErrors} de '../server';
import {Utilisateur} de '../../../interfaces/user.interface';
importer {randomBytes} depuis 'crypto';
const generateToken = () => randomBytes (8) .toString ('hex');
interface d'exportation AuthResponse {
jeton: chaîne;
utilisateur: utilisateur;
}
const login = (schéma: any, req: Request): AuthResponse | Réponse => {
const {nom d'utilisateur, mot de passe} = JSON.parse (req.requestBody);
const user = schema.users.findBy ({nom d'utilisateur});
if (! utilisateur) {
return handleErrors (null, 'Aucun utilisateur avec ce nom d'utilisateur n'existe');
}
if (mot de passe! == user.password) {
return handleErrors (null, 'Le mot de passe est incorrect');
}
jeton const = generateToken ();
revenir {
user: user.attrs en tant qu'utilisateur,
jeton,
};
};
const signup = (schéma: any, req: Request): AuthResponse | Réponse => {
const data = JSON.parse (req.requestBody);
const exUser = schema.users.findBy ({nom d'utilisateur: data.username});
if (exUser) {
return handleErrors (null, 'Un utilisateur avec ce nom d'utilisateur existe déjà.');
}
const user = schema.users.create (données);
jeton const = generateToken ();
revenir {
user: user.attrs en tant qu'utilisateur,
jeton,
};
};
export par défaut {
s'identifier,
s'inscrire,
};
Les méthodes login
et signup
reçoivent ici une classe Schema
et un faux objet Request
et, après validation du mot de passe ou vérification que la connexion n'existe pas déjà, renvoie l'utilisateur existant ou un nouvel utilisateur respectivement. Nous utilisons l'objet Schema
pour interagir avec l'ORM de Mirage, tandis que l'objet Request
contient des informations sur la requête interceptée, y compris le corps de la requête et les en-têtes.
Ensuite, ajoutons des méthodes de travail avec agendas et entrées d'agenda. Créez un fichier nommé diary.ts dans votre répertoire routes :
touch diary.ts
Mettez à jour le fichier avec les méthodes suivantes pour utiliser les ressources Diary
:
export const create = (
schéma: tout,
req: Demande
): {utilisateur: Utilisateur; journal: Journal} | Réponse => {
essayez {
const {title, type, userId} = JSON.parse (req.requestBody) as Partial <
Diary
>;
const exUser = schema.users.findBy ({id: userId});
if (! exUser) {
return handleErrors (null, 'Aucun utilisateur n'existe.');
}
const now = dayjs (). format ();
const diary = exUser.createDiary ({
Titre,
type,
createdAt: maintenant,
mis à jour à: maintenant,
});
revenir {
utilisateur: {
... exUser.attrs,
},
agenda: diary.attrs,
};
} catch (erreur) {
return handleErrors (erreur, 'Impossible de créer le journal.');
}
};
export const updateDiary = (schéma: any, req: Request): Journal | Réponse => {
essayez {
const diary = schema.diaries.find (req.params.id);
const data = JSON.parse (req.requestBody) as Partial ;
const now = dayjs (). format ();
diary.update ({
...Les données,
mis à jour à: maintenant,
});
retourne diary.attrs comme Journal;
} catch (erreur) {
return handleErrors (erreur, 'Échec de la mise à jour du journal.');
}
};
export const getDiaries = (schéma: any, req: Request): Agenda [] | Réponse => {
essayez {
const user = schema.users.find (req.params.id);
renvoie user.diary sous forme de journal [];
} catch (erreur) {
return handleErrors (error, 'Impossible d'obtenir les journaux des utilisateurs.');
}
};
Ensuite, ajoutons quelques méthodes pour travailler avec les entrées du journal:
export const addEntry = (
schéma: tout,
req: Demande
): {agenda: Agenda; entrée: Entrée} | Réponse => {
essayez {
const diary = schema.diaries.find (req.params.id);
const {title, content} = JSON.parse (req.requestBody) as Partial ;
const now = dayjs (). format ();
entrée const = journal.createEntry ({
Titre,
contenu,
createdAt: maintenant,
mis à jour à: maintenant,
});
diary.update ({
... agenda.attrs,
mis à jour à: maintenant,
});
revenir {
agenda: diary.attrs,
entrée: entry.attrs,
};
} catch (erreur) {
return handleErrors (error, 'Impossible d'enregistrer l'entrée.');
}
};
export const getEntries = (
schéma: tout,
req: Demande
): {entrées: Entrée []} | Réponse => {
essayez {
const diary = schema.diaries.find (req.params.id);
retour journal.entry;
} catch (erreur) {
return handleErrors (error, 'Impossible d'obtenir les entrées du journal.');
}
};
export const updateEntry = (schéma: any, req: Request): Entrée | Réponse => {
essayez {
entrée const = schema.entries.find (req.params.id);
const data = JSON.parse (req.requestBody) as Partial ;
const now = dayjs (). format ();
entry.update ({
...Les données,
mis à jour à: maintenant,
});
retourne entry.attrs comme Entry;
} catch (erreur) {
return handleErrors (erreur, 'Échec de la mise à jour de l'entrée.');
}
};
Enfin, ajoutons les importations nécessaires en haut du fichier:
import {Response, Request} from 'miragejs';
import {handleErrors} de '../server';
import {Agenda} de '../../../interfaces/diary.interface';
import {Entrée} de '../../../interfaces/entry.interface';
importer les jours de «dayjs»;
import {Utilisateur} de '../../../interfaces/user.interface';
Dans ce fichier, nous avons exporté des méthodes pour travailler avec les modèles Agenda
et Entry
. Dans la méthode create
nous appelons une méthode nommée user.createDiary ()
pour enregistrer un nouveau journal et l'associer à un compte utilisateur.
Le addEntry
] et updateEntry
créent et associent correctement une nouvelle entrée à un journal ou mettent respectivement à jour les données d'une entrée existante. Ce dernier met également à jour la propriété updatedAt
de l’entrée avec l’horodatage actuel. La méthode updateDiary
met également à jour un journal avec l'horodatage de la modification. Plus tard, nous trierons les enregistrements que nous recevons de notre requête réseau avec cette propriété.
Nous avons également une méthode getDiaries
qui récupère les journaux d'un utilisateur et une méthode getEntries
qui récupère les entrées d'un journal sélectionné.
Nous pouvons maintenant mettre à jour notre serveur pour utiliser les méthodes que nous venons de créer. Ouvrez server.ts pour inclure les fichiers:
import {Server, Model, Factory, comesTo, hasMany, Response} from 'miragejs';
importer un utilisateur depuis './routes/user';
import * comme agenda depuis './routes/diary';
Ensuite, mettez à jour la propriété route
du serveur avec les routes que nous voulons gérer:
export const setupServer = (env ?: string): Server => {
retourner un nouveau serveur ({
// ...
routes (): void {
this.urlPrefix = 'https://diaries.app';
this.get ('/ journaux / entrées /: id', diary.getEntries);
this.get ('/ diaries /: id', diary.getDiaries);
this.post ('/ auth / login', user.login);
this.post ('/ auth / signup', user.signup);
this.post ('/ diaries /', diary.create);
this.post ('/ journaux / entrée /: id', journal.addEntry);
this.put ('/ diaries / entrée /: id', diary.updateEntry);
this.put ('/ diaries /: id', diary.updateDiary);
},
});
};
Avec ce changement, lorsqu'une requête réseau de notre application correspond à l'un des gestionnaires de route, Mirage intercepte la requête et appelle les fonctions de gestionnaire de route respectives.
Ensuite, nous allons faire connaître le serveur à notre application. . Ouvrez src / index.tsx et importez la méthode setupServer ()
:
import {setupServer} depuis './services/mirage/server';
Et ajoutez le code suivant avant ReactDOM.render ()
:
if (process.env.NODE_ENV === 'development') {
setupServer ();
}
La vérification dans le bloc de code ci-dessus garantit que notre serveur Mirage ne fonctionnera que pendant que nous sommes en mode développement.
Une dernière chose que nous devons faire avant de passer aux bits Redux est de configurer un Axios personnalisé instance à utiliser dans notre application. Cela aidera à réduire la quantité de code que nous devrons écrire plus tard.
Créez un fichier nommé api.ts sous src / services et ajoutez-y le code suivant:
import axios, {AxiosInstance, AxiosResponse, AxiosError} de 'axios';
import {showAlert} de '../util';
const http: AxiosInstance = axios.create ({
baseURL: 'https://diaries.app',
});
http.defaults.headers.post ['Content-Type'] = 'application / json';
http.interceptors.response.use (
async (réponse: AxiosResponse): Promise => {
if (response.status> = 200 && response.status < 300) {
return response.data;
}
},
(error: AxiosError) => {
const {réponse, requête}: {
réponse?: AxiosResponse;
request?: XMLHttpRequest;
} = erreur;
if (réponse) {
if (response.status> = 400 && response.status <500) {
showAlert (response.data?.data?.message, 'erreur');
return null;
}
} else if (demande) {
showAlert ('La demande a échoué. Veuillez réessayer.', 'erreur');
return null;
}
return Promise.reject (erreur);
}
);
export par défaut http;
Dans ce fichier, nous exportons une instance Axios modifiée pour inclure l'URL d'API de notre application, https://diaries.app . Nous avons configuré un intercepteur pour gérer les réponses de succès et d'erreur, et nous affichons les messages d'erreur à l'aide d'un sweetalert
toast que nous configurerons à l'étape suivante.
Créez un fichier nommé util.ts
dans votre répertoire src et collez-y le code suivant:
import Swal, {SweetAlertIcon} from 'sweetalert2';
export const showAlert = (titleText = 'Quelque chose est arrivé.', alertType ?: SweetAlertIcon): void => {
Swal.fire ({
titleText,
position: 'haut de gamme',
minuterie: 3000,
timerProgressBar: vrai,
toast: vrai,
showConfirmButton: faux,
showCancelButton: vrai,
cancelButtonText: 'Ignorer',
icône: alertType,
showClass: {
popup: 'swal2-noanimation',
toile de fond: 'swal2-noanimation',
},
hideClass: {
apparaitre: '',
toile de fond: '',
},
});
};
Ce fichier exporte une fonction qui affiche un toast chaque fois qu'elle est appelée. La fonction accepte des paramètres pour vous permettre de définir le message et le type de toast. Par exemple, nous affichons un toast d'erreur dans l'intercepteur d'erreur de réponse Axios comme ceci:
showAlert (response.data?.data?.message, 'error');
Désormais, lorsque nous faisons des requêtes depuis notre application en mode développement, elles seront interceptées et gérées par Mirage à la place. Dans la section suivante, nous allons configurer notre boutique Redux en utilisant la boîte à outils Redux.
Configurer une boutique Redux
Dans cette section, nous allons configurer notre boutique en utilisant les exportations suivantes de la boîte à outils Redux: configureStore ()
getDefaultMiddleware ()
et createSlice ()
. Avant de commencer, nous devrions examiner en détail ce que font ces exportations.
configureStore ()
est une abstraction de la fonction Redux createStore ()
qui simplifie votre code. Il utilise createStore ()
en interne pour configurer votre boutique avec quelques outils de développement utiles:
export const store = configureStore ({
reducer: rootReducer, // une seule fonction de réduction ou un objet de réducteurs de tranche
});
La fonction createSlice ()
permet de simplifier le processus de création de créateurs d'action et de réducteurs de tranches. Il accepte un état initial, un objet plein de fonctions de réducteur et un «nom de tranche», et génère automatiquement des créateurs d'actions et des types d'actions correspondant aux réducteurs et à votre état. Il renvoie également une fonction de réducteur unique, qui peut être transmise à la fonction combinerReducers () de Redux en tant que «réducteur de tranche».
Rappelez-vous que l'état est un seul arbre et qu'un seul réducteur de racine gère les changements à cet arbre. Pour la maintenabilité, il est recommandé de diviser votre réducteur racine en «tranches» et de demander à un «réducteur de tranche» de fournir une valeur initiale et de calculer les mises à jour pour une tranche correspondante de l'état. Ces tranches peuvent être réunies en une seule fonction de réduction en utilisant combineReducers ()
.
Il existe des options supplémentaires pour configurer le magasin . Par exemple, vous pouvez transmettre un tableau de votre propre middleware à configureStore ()
ou démarrer votre application à partir d'un état enregistré à l'aide de l'option preloadedState
. Lorsque vous fournissez l'option middleware
vous devez définir all le middleware que vous souhaitez ajouter au magasin. Si vous souhaitez conserver les valeurs par défaut lors de la configuration de votre boutique, vous pouvez utiliser getDefaultMiddleware ()
pour obtenir la liste par défaut des middlewares:
export const store = configureStore ({
// ...
middleware: [...getDefaultMiddleware(), customMiddleware],
});
Nous allons maintenant mettre en place notre boutique. Nous adopterons une approche «ducks-style» pour structurer nos fichiers, en suivant spécifiquement les directives pratiques de l'exemple d'application Github Issues . Nous organiserons notre code de manière à ce que les composants associés, ainsi que les actions et les réducteurs, résident dans le même répertoire. L'objet d'état final ressemblera à ceci:
type RootState = {
auth: {
jeton: chaîne | nul;
isAuthenticated: booléen;
};
agendas: Diary [];
entrées: Entrée [];
utilisateur: Utilisateur | nul;
éditeur: {
canEdit: booléen;
currentEditing: Entrée | nul;
activeDiaryId: chaîne | nul;
};
}
Pour commencer, créez un nouveau répertoire nommé features sous votre répertoire src :
# ~ / diaries-app / src
Fonctionnalités de mkdir
Ensuite, cd
dans les fonctionnalités et créez des répertoires nommés auth diary et entry :
cd features
entrée de journal d'authentification mkdir
cd
dans le répertoire auth et créez un fichier nommé authSlice.ts :
cd auth
# ~ / diaries-app / src / features / auth
touchez authSlice.ts
Ouvrez le fichier et collez-y ce qui suit:
import {createSlice, PayloadAction} de '@ reduxjs / toolkit';
interface AuthState {
jeton: chaîne | nul;
isAuthenticated: booléen;
}
const initialState: AuthState = {
jeton: nul,
isAuthenticated: faux,
};
const auth = createSlice ({
nom: 'auth',
Etat initial,
réducteurs: {
saveToken (état, {payload}: PayloadAction ) {
if (charge utile) {
state.token = charge utile;
}
},
clearToken (état) {
state.token = null;
},
setAuthState (état, {charge utile}: PayloadAction ) {
state.isAuthenticated = charge utile;
},
},
});
export const {saveToken, clearToken, setAuthState} = auth.actions;
export par défaut auth.reducer;
Dans ce fichier, nous créons une tranche pour la propriété auth
de l'état de notre application à l'aide de la fonction createSlice ()
introduite précédemment. La propriété reducers
contient une carte de fonctions de réduction pour mettre à jour les valeurs dans la tranche d'authentification. L'objet renvoyé contient des créateurs d'actions générés automatiquement et un réducteur de tranche unique. Nous aurions besoin de les utiliser dans d'autres fichiers donc, en suivant le «modèle des canards», nous effectuons des exportations nommées des créateurs d'action et une exportation par défaut de la fonction de réduction.
Configurons les tranches de réduction restantes en fonction de l'application état que nous avons vu plus tôt. Commencez par créer un fichier nommé userSlice.ts dans le répertoire auth et ajoutez-y le code suivant:
import {createSlice, PayloadAction} de '@ reduxjs / toolkit';
import {Utilisateur} de '../../interfaces/user.interface';
utilisateur const = createSlice ({
nom: 'utilisateur',
initialState: null en tant qu'utilisateur | nul,
réducteurs: {
setUser (état, {payload}: PayloadAction ) {
return state = (payload! = null)? charge utile: null;
},
},
});
export const {setUser} = user.actions;
exporter l'utilisateur par défaut.reducer;
Cela crée un réducteur de tranche pour la propriété user
dans le magasin de l'application. La fonction de réduction setUser
accepte une charge utile contenant des données utilisateur et met à jour l'état avec elle. Lorsqu'aucune donnée n'est transmise, nous définissons la propriété utilisateur de l'état sur null
.
Ensuite, créez un fichier nommé diariesSlice.ts sous src / features / diary ]:
# ~ / diaries-app / src / features
agenda cd
journal tactileSlice.ts
Ajoutez le code suivant au fichier:
import {createSlice, PayloadAction} de '@ reduxjs / toolkit';
import {Agenda} de '../../interfaces/diary.interface';
const diaries = createSlice ({
nom: 'agendas',
initialState: [] comme journal [],
réducteurs: {
addDiary (état, {payload}: PayloadAction ) {
const diariesToSave = payload.filter ((journal) => {
return state.findIndex ((item) => item.id === diary.id) === -1;
});
state.push (... diariesToSave);
},
updateDiary (état, {payload}: PayloadAction ) {
const {id} = charge utile;
const diaryIndex = state.findIndex ((diary) => diary.id === id);
if (diaryIndex! == -1) {
state.splice (diaryIndex, 1, charge utile);
}
},
},
});
export const {addDiary, updateDiary} = diaries.actions;
exporter les journaux par défaut.reducer;
La propriété «diaries» de notre état est un tableau contenant les journaux de l'utilisateur, donc nos fonctions de réduction ici fonctionnent toutes sur l'objet d'état qu'elles reçoivent en utilisant des méthodes de tableau. Remarquez ici que nous écrivons du code «mutatif» normal lorsque nous travaillons sur l'état. Cela est possible car les fonctions de réduction que nous créons à l’aide de la méthode createSlice ()
sont encapsulées avec la méthode produire ()
d’Immer. Cela aboutit à ce qu'Immer renvoie un résultat correct et immuablement mis à jour pour notre état, que nous écrivions du code mutatif.
Ensuite, créez un fichier nommé entriesSlice.ts sous src / features / entry :
# ~ / diaries-app / src / features
entrée mkdir
entrée cd
entrées tactilesSlice.ts
Ouvrez le fichier et ajoutez le code suivant:
import {createSlice, PayloadAction} de '@ reduxjs / toolkit';
import {Entrée} de '../../interfaces/entry.interface';
entrées const = createSlice ({
nom: 'entrées',
initialState: [] comme entrée [],
réducteurs: {
setEntries (état, {charge utile}: PayloadAction ) {
return (état = charge utile! = null? charge utile: []);
},
updateEntry (état, {payload}: PayloadAction ) {
const {id} = charge utile;
index const = state.findIndex ((e) => e.id === id);
if (index! == -1) {
state.splice (index, 1, charge utile);
}
},
},
});
export const {setEntries, updateEntry} = entries.actions;
exporter les entrées par défaut.reducer;
Les fonctions de réduction ici ont une logique similaire aux fonctions de réduction de la tranche précédente. La propriété entries
est également un tableau, mais elle ne contient que les entrées d'un seul journal. Dans notre application, ce sera le journal actuellement dans le focus de l'utilisateur.
Enfin, créez un fichier nommé editorSlice.ts dans src / features / entry et ajoutez ce qui suit:
import {createSlice, PayloadAction} from '@ reduxjs / toolkit';
import {Entrée} de '../../interfaces/entry.interface';
interface EditorState {
canEdit: booléen;
currentEditing: Entrée | nul;
activeDiaryId: chaîne | nul;
}
const initialState: EditorState = {
canEdit: faux,
currentEditing: null,
activeDiaryId: null,
};
éditeur const = createSlice ({
nom: 'éditeur',
Etat initial,
réducteurs: {
setCanEdit (état, {charge utile}: PayloadAction ) {
state.canEdit = charge utile! = null? charge utile:! state.canEdit;
},
setCurrentlyEditing (état, {charge utile}: PayloadAction ) {
state.currentlyEditing = charge utile;
},
setActiveDiaryId (état, {payload}: PayloadAction ) {
state.activeDiaryId = charge utile;
},
},
});
export const {setCanEdit, setCurrentlyEditing, setActiveDiaryId} = editor.actions;
export par défaut editor.reducer;
Ici, nous avons une part de la propriété editor
dans l'état. Nous utiliserons les propriétés de cet objet pour vérifier si l'utilisateur veut passer en mode édition, à quel journal appartient l'entrée éditée et à quelle entrée va être éditée.
Pour tout mettre ensemble, créez un fichier nommé rootReducer.ts dans le répertoire src avec le contenu suivant:
import {combineReducers} de '@ reduxjs / toolkit';
import authReducer de './features/auth/authSlice';
importer userReducer depuis './features/auth/userSlice';
import diariesReducer de './features/diary/diariesSlice';
importer des entréesReducer de './features/entry/entriesSlice';
import editorReducer de './features/entry/editorSlice';
const rootReducer = combineReducers ({
auth: authReducer,
agendas: agendas
entrées: entréesReducer,
utilisateur: userReducer,
éditeur: editorReducer,
});
type d'exportation RootState = ReturnType ;
exporter rootReducer par défaut;
Dans ce fichier, nous avons combiné nos réducteurs de tranche en un seul réducteur de racine avec la fonction combineReducers ()
. We’ve also exported the RootState
type, which will be useful later when we’re selecting values from the store. We can now use the root reducer (the default export of this file) to set up our store.
Create a file named store.ts with the following contents:
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './rootReducer';
import { useDispatch } from 'react-redux';
const store = configureStore({
reducer: rootReducer,
});
type AppDispatch = typeof store.dispatch;
export const useAppDispatch = () => useDispatch();
export default store;
With this, we’ve created a store using the configureStore()
export from Redux toolkit. We’ve also exported an hook called useAppDispatch()
which merely returns a typed useDispatch()
hook.
Next, update the imports in index.tsx to look like the following:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './app/App';
import * as serviceWorker from './serviceWorker';
import { setupServer } from './services/mirage/server';
import { Provider } from 'react-redux';
import store from './store';
// ...
Finally, make the store
available to the app’s components by wrapping
(the top-level component) with
:
ReactDOM.render(
,
document.getElementById('root')
);
Now, if you start your app and you navigate to http://localhost:3000 with the Redux Dev Tools extension enabled, you should see the following in your app’s state:

Great work so far, but we’re not quite finished yet. In the next section, we will design the app’s User Interface and add functionality using the store we’ve just created.
Designing The Application User Interface
To see Redux in action, we are going to build a demo app. In this section, we will connect our components to the store we’ve created and learn to dispatch actions and modify the state using reducer functions. We will also learn how to read values from the store. Here’s what our Redux-powered application will look like.


Setting up the Authentication Feature
To get started, move App.tsx and its related files from the src directory to its own directory like this:
# ~/diaries-app/src
mkdir app
mv App.tsx App.test.tsx app
You can delete the App.css and logo.svg files as we won’t be needing them.
Next, open the App.tsx file and replace its contents with the following:
import React, { FC, lazy, Suspense } from 'react';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { RootState } from '../rootReducer';
const Auth = lazy(() => import('../features/auth/Auth'));
const Home = lazy(() => import('../features/home/Home'));
const App: FC = () => {
const isLoggedIn = useSelector(
(state: RootState) => state.auth.isAuthenticated
);
return (
<Suspense fallback={Loading...
}>
{isLoggedIn ? : }
);
};
export default App;
Here we have set up our app to render an
component if the user is unauthenticated, or otherwise render a
component. We haven’t created either of these components yet, so let’s fix that. Create a file named Auth.tsx under src/features/auth and add the following contents to the file:
import React, { FC, useState } from 'react';
import { useForm } from 'react-hook-form';
import { User } from '../../interfaces/user.interface';
import * as Yup from 'yup';
import http from '../../services/api';
import { saveToken, setAuthState } from './authSlice';
import { setUser } from './userSlice';
import { AuthResponse } from '../../services/mirage/routes/user';
import { useAppDispatch } from '../../store';
const schema = Yup.object().shape({
username: Yup.string()
.required('What? No username?')
.max(16, 'Username cannot be longer than 16 characters'),
password: Yup.string().required('Without a password, "None shall pass!"'),
email: Yup.string().email('Please provide a valid email address (abc@xy.z)'),
});
const Auth: FC = () => {
const { handleSubmit, register, errors } = useForm({
validationSchema: schema,
});
const [isLogin, setIsLogin] = useState(true);
const [loading, setLoading] = useState(false);
const dispatch = useAppDispatch();
const submitForm = (data: User) => {
const path = isLogin ? '/auth/login' : '/auth/signup';
http
.post(path, data)
.then((res) => {
if (res) {
const { user, token } = res;
dispatch(saveToken(token));
dispatch(setUser(user));
dispatch(setAuthState(true));
}
})
.catch((error) => {
console.log(error);
})
.finally(() => {
setLoading(false);
});
};
return (
);
};
export default Auth;
In this component, we have set up a form for users to log in, or to create an account. Our form fields are validated using Yup and, on successfully authenticating a user, we use our useAppDispatch
hook to dispatch the relevant actions. You can see the dispatched actions and the changes made to your state in the Redux DevTools Extension:

Finally, create a file named Home.tsx under src/features/home and add the following code to the file:
import React, { FC } from 'react';
const Home: FC = () => {
return (
Welcome user!
);
};
export default Home;
For now, we are just displaying some text to the authenticated user. As we build the rest of our application, we will be updating this file.
Setting up the Editor
The next component we are going to build is the editor. Though basic, we will enable support for rendering markdown content using the markdown-to-jsx
library we installed earlier.
First, create a file named Editor.tsx in the src/features/entry directory. Then, add the following code to the file:
import React, { FC, useState, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { RootState } from '../../rootReducer';
import Markdown from 'markdown-to-jsx';
import http from '../../services/api';
import { Entry } from '../../interfaces/entry.interface';
import { Diary } from '../../interfaces/diary.interface';
import { setCurrentlyEditing, setCanEdit } from './editorSlice';
import { updateDiary } from '../diary/diariesSlice';
import { updateEntry } from './entriesSlice';
import { showAlert } from '../../util';
import { useAppDispatch } from '../../store';
const Editor: FC = () => {
const { currentlyEditing: entry, canEdit, activeDiaryId } = useSelector(
(state: RootState) => state.editor
);
const [editedEntry, updateEditedEntry] = useState(entry);
const dispatch = useAppDispatch();
const saveEntry = async () => {
if (activeDiaryId == null) {
return showAlert('Please select a diary.', 'warning');
}
if (entry == null) {
http
.post(
`/diaries/entry/${activeDiaryId}`,
editedEntry
)
.then((data) => {
if (data != null) {
const { diary, entry: _entry } = data;
dispatch(setCurrentlyEditing(_entry));
dispatch(updateDiary(diary));
}
});
} else {
http
.put(`diaries/entry/${entry.id}`, editedEntry)
.then((_entry) => {
if (_entry != null) {
dispatch(setCurrentlyEditing(_entry));
dispatch(updateEntry(_entry));
}
});
}
dispatch(setCanEdit(false));
};
useEffect(() => {
updateEditedEntry(entry);
}, [entry]);
return (
{entry && !canEdit ? (
{entry.title}
{
e.preventDefault();
if (entry != null) {
dispatch(setCanEdit(true));
}
}}
style={{ marginLeft: '0.4em' }}
>
(Edit)
) : (
{
if (editedEntry) {
updateEditedEntry({
...editedEntry,
title: e.target.value,
});
} else {
updateEditedEntry({
title: e.target.value,
content: '',
});
}
}}
/>
)}
{entry && !canEdit ? (
{entry.content}
) : (
<>
);
};
export default Editor;
Let’s break down what’s happening in the Editor
component.
First, we are picking some values (with correctly inferred types) from the app’s state using the useSelector()
hook from react-redux
. In the next line, we have a stateful value called editedEntry
whose initial value is set to the editor.currentlyEditing
property we’ve selected from the store.
Next, we have the saveEntry
function which updates or creates a new entry in the API, and dispatches the respective Redux action.
Finally, we have a useEffect
that is fired when the editor.currentlyEditing
property changes. Our editor’s UI (in the component’s return function) has been set up to respond to changes in the state. For example, rendering the entry’s content as JSX elements when the user isn’t editing.
With that, the app’s Entry
feature should be completely set up. In the next section, we will finish building the Diary
feature and then import the main components in the Home
component we created earlier.
Final Steps
To finish up our app, we will first create components for the Diary
feature. Then, we will update the Home
component with the primary exports from the Diary
and Entry
features. Finally, we will add some styling to give our app the required pizzazz!
Let’s start by creating a file in src/features/diary named DiaryTile.tsx. This component will present information about a diary and its entries, and allow the user to edit the diary’s title. Add the following code to the file:
import React, { FC, useState } from 'react';
import { Diary } from '../../interfaces/diary.interface';
import http from '../../services/api';
import { updateDiary } from './diariesSlice';
import { setCanEdit, setActiveDiaryId, setCurrentlyEditing } from '../entry/editorSlice';
import { showAlert } from '../../util';
import { Link } from 'react-router-dom';
import { useAppDispatch } from '../../store';
interface Props {
diary: Diary;
}
const buttonStyle: React.CSSProperties = {
fontSize: '0.7em',
margin: '0 0.5em',
};
const DiaryTile: FC = (props) => {
const [diary, setDiary] = useState(props.diary);
const [isEditing, setIsEditing] = useState(false);
const dispatch = useAppDispatch();
const totalEntries = props.diary?.entryIds?.length;
const saveChanges = () => {
http
.put(`/diaries/${diary.id}`, diary)
.then((diary) => {
if (diary) {
dispatch(updateDiary(diary));
showAlert('Saved!', 'success');
}
})
.finally(() => {
setIsEditing(false);
});
};
return (
setIsEditing(true)}
style={{
cursor: 'pointer',
}}
>
{isEditing ? (
{
setDiary({
...diary,
title: e.target.value,
});
}}
onKeyUp={(e) => {
if (e.key === 'Enter') {
saveChanges();
}
}}
/>
) : (
{diary.title}
)}
{totalEntries ?? '0'} saved entries
);
};
export default DiaryTile;
In this file, we receive a diary object as a prop and display the data in our component. Notice that we use local state and component props for our data display here. That’s because you don’t have to manage all your app’s state using Redux. Sharing data using props, and maintaining local state in your components is acceptable and encouraged in some cases.
Next, let’s create a component that will display a list of a diary’s entries, with the last updated entries at the top of the list. Ensure you are in the src/features/diary directory, then create a file named DiaryEntriesList.tsx and add the following code to the file:
import React, { FC, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { RootState } from '../../rootReducer';
import http from '../../services/api';
import { Entry } from '../../interfaces/entry.interface';
import { setEntries } from '../entry/entriesSlice';
import { setCurrentlyEditing, setCanEdit } from '../entry/editorSlice';
import dayjs from 'dayjs';
import { useAppDispatch } from '../../store';
const DiaryEntriesList: FC = () => {
const { entries } = useSelector((state: RootState) => state);
const dispatch = useAppDispatch();
const { id } = useParams();
useEffect(() => {
if (id != null) {
http
.get(`/diaries/entries/${id}`)
.then(({ entries: _entries }) => {
if (_entries) {
const sortByLastUpdated = _entries.sort((a, b) => {
return dayjs(b.updatedAt).unix() - dayjs(a.updatedAt).unix();
});
dispatch(setEntries(sortByLastUpdated));
}
});
}
}, [id, dispatch]);
return (
← Go Back
{entries.map((entry) => (
- {
dispatch(setCurrentlyEditing(entry));
dispatch(setCanEdit(true));
}}
>
{entry.title}
))}
);
};
export default DiaryEntriesList;
Here, we subscribe to the entries property of our app’s state, and have our effect fetch a diary’s entry only run when a property, id
changes. This property’s value is gotten from our URL as a path parameter using the useParams()
hook from react-router
. In the next step, we will create a component that will enable users to create and view diaries, as well as render a diary’s entries when it is in focus.
Create a file named Diaries.tsx while still in the same directory, and add the following code to the file:
import React, { FC, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { RootState } from '../../rootReducer';
import http from '../../services/api';
import { Diary } from '../../interfaces/diary.interface';
import { addDiary } from './diariesSlice';
import Swal from 'sweetalert2';
import { setUser } from '../auth/userSlice';
import DiaryTile from './DiaryTile';
import { User } from '../../interfaces/user.interface';
import { Route, Switch } from 'react-router-dom';
import DiaryEntriesList from './DiaryEntriesList';
import { useAppDispatch } from '../../store';
import dayjs from 'dayjs';
const Diaries: FC = () => {
const dispatch = useAppDispatch();
const diaries = useSelector((state: RootState) => state.diaries);
const user = useSelector((state: RootState) => state.user);
useEffect(() => {
const fetchDiaries = async () => {
if (user) {
http.get(`diaries/${user.id}`).then((data) => {
if (data && data.length > 0) {
const sortedByUpdatedAt = data.sort((a, b) => {
return dayjs(b.updatedAt).unix() - dayjs(a.updatedAt).unix();
});
dispatch(addDiary(sortedByUpdatedAt));
}
});
}
};
fetchDiaries();
}, [dispatch, user]);
const createDiary = async () => {
const result = await Swal.mixin({
input: 'text',
confirmButtonText: 'Next →',
showCancelButton: true,
progressSteps: ['1', '2'],
}).queue([
{
titleText: 'Diary title',
input: 'text',
},
{
titleText: 'Private or public diary?',
input: 'radio',
inputOptions: {
private: 'Private',
public: 'Public',
},
inputValue: 'private',
},
]);
if (result.value) {
const { value } = result;
const {
diary,
user: _user,
} = await http.post<Partial{ diary: Diary; user: User }>('/diaries/', {
title: value[0],
type: value[1],
userId: user?.id,
});
if (diary && user) {
dispatch(addDiary([diary] as Diary[]));
dispatch(addDiary([diary] as Diary[]));
dispatch(setUser(_user));
return Swal.fire({
titleText: 'All done!',
confirmButtonText: 'OK!',
});
}
}
Swal.fire({
titleText: 'Cancelled',
});
};
return (
{diaries.map((diary, idx) => (
))}
);
};
export default Diaries;
In this component, we have a function to fetch the user’s diaries inside a useEffect
hook, and a function to create a new diary. We also render our components in react-router
’s
component, rendering a diary’s entries if its id
matches the path param in the route /diary/:id
or otherwise rendering a list of the user’s diaries.
To wrap things up, let’s update the Home.tsx
component. First, update the imports to look like the following:
import React, { FC } from 'react';
import Diaries from '../diary/Diaries';
import Editor from '../entry/Editor';
Then, change the component’s return statement to the following:
return (
Finally, replace the contents of the index.css file in your app’s src directory with the following code:
:root {
--primary-color: #778899;
--error-color: #f85032;
--text-color: #0d0d0d;
--transition: all ease-in-out 0.3s;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html, body, #root {
height: 100%;
}
*, *:before, *:after {
box-sizing: border-box;
}
.auth {
display: flex;
align-items: center;
height: 100%;
}
.card {
background: #fff;
padding: 3rem;
text-align: center;
box-shadow: 2px 8px 12px rgba(0, 0, 0, 0.1);
max-width: 450px;
width: 90%;
margin: 0 auto;
}
.inputWrapper {
margin: 1rem auto;
width: 100%;
}
input:not([type='checkbox']), button {
border-radius: 0.5rem;
width: 100%;
}
input:not([type='checkbox']), textarea {
border: 2px solid rgba(0, 0, 0, 0.1);
padding: 1em;
color: var(--text-color);
transition: var(--transition);
}
input:not([type='checkbox']):focus, textarea:focus {
outline: none;
border-color: var(--primary-color);
}
button {
appearance: none;
border: 1px solid var(--primary-color);
color: #fff;
background-color: var(--primary-color);
text-transform: uppercase;
font-weight: bold;
outline: none;
cursor: pointer;
padding: 1em;
box-shadow: 1px 4px 6px rgba(0, 0, 0, 0.1);
transition: var(--transition);
}
button.secondary {
color: var(--primary-color);
background-color: #fff;
border-color: #fff;
}
button:hover, button:focus {
box-shadow: 1px 6px 8px rgba(0, 0, 0, 0.1);
}
.error {
margin: 0;
margin-top: 0.2em;
font-size: 0.8em;
color: var(--error-color);
animation: 0.3s ease-in-out forwards fadeIn;
}
.two-cols {
display: flex;
flex-wrap: wrap;
height: 100vh;
}
.two-cols .left {
border-right: 1px solid rgba(0, 0, 0, 0.1);
height: 100%;
overflow-y: scroll;
}
.two-cols .right {
overflow-y: auto;
}
.title {
font-size: 1.3rem;
}
.subtitle {
font-size: 0.9rem;
opacity: 0.85;
}
.title, .subtitle {
margin: 0;
}
.diary-tile {
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
padding: 1em;
}
.editor {
height: 100%;
padding: 1em;
}
.editor input {
width: 100%;
}
.editor textarea {
width: 100%;
height: calc(100vh - 160px);
}
.entries ul {
list-style: none;
padding: 0;
}
.entries li {
border-top: 1px solid rgba(0, 0, 0, 0.1);
padding: 0.5em;
cursor: pointer;
}
.entries li:nth-child(even) {
background: rgba(0, 0, 0, 0.1);
}
@media (min-width: 768px) {
.two-cols .left {
width: 25%;
}
.two-cols .right {
width: 75%;
}
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 0.8;
}
}
That’s it! You can now run npm start
or yarn start
and check out the final app at http://localhost:3000.

Conclusion
In this guide, you have learned how to rapidly develop applications using Redux. You also learned about good practices to follow when working with Redux and React, in order to make debugging and extending your applications easier. This guide is by no means extensive as there are still ongoing discussions surrounding Redux and some of its concepts. Please check out the Redux and React-Redux docs if you’d like to learn more about using Redux in your React projects.
References

Source link