18 KiB
Créer une application bancaire Partie 4: Concepts de gestion d'état
Quiz préalable
Introduction
Au fur et à mesure qu’une application Web se développe, il devient difficile de suivre tous les flux de données. Quel code obtient les données, quelle page les consomme, où et quand doit-il être mis à jour... il est facile de se retrouver avec un code désordonné et difficile à maintenir. Cela est particulièrement vrai lorsque vous devez partager des données entre différentes pages de votre application, par exemple des données utilisateur. Le concept de gestion de l’état a toujours existé dans toutes sortes de programmes, mais comme les applications Web ne cessent de croître en complexité, c’est maintenant un point clé à prendre en compte pendant le développement.
Dans cette dernière partie, nous examinerons l’application que nous avons créée pour repenser la façon dont l’état est géré, permettant la prise en charge de l’actualisation du navigateur à tout moment et la persistance des données entre les sessions utilisateur.
Prérequis
Vous devez avoir terminé la récupération des données de l’application web pour cette leçon. Vous devez également installer Node.js et exécuter l’API du serveur localement afin que vous puissiez gérer les données du compte.
Vous pouvez tester que le serveur fonctionne correctement en exécutant cette commande dans un terminal:
curl http://localhost:5000/api
# -> doit renvoyer "Bank API v1.0.0" comme résultat
Repenser la gestion des états
Dans la leçon précédente, nous avons introduit un concept basique d’état dans notre application avec la variable globale account
qui contient les données bancaires de l’utilisateur actuellement connecté. Cependant, notre mise en œuvre actuelle présente certains défauts. Essayez d’actualiser la page lorsque vous êtes sur le tableau de bord. Que se passe-t-il?
Il y a 3 problèmes avec le code actuel:
- L’état n’est pas persistant, car une actualisation du navigateur vous ramène à la page de connexion.
- Il existe plusieurs fonctions qui modifient l’état. Au fur et à mesure que l’application se développe, il peut être difficile de suivre les modifications et il est facile d’oublier d’en mettre à jour une.
- L’état n’est pas nettoyé, donc lorsque vous cliquez sur Logout les données du compte sont toujours là même si vous êtes sur la page de connexion.
Nous pourrions mettre à jour notre code pour résoudre ces problèmes un par un, mais cela créerait plus de duplication de code et rendrait l’application plus complexe et difficile à maintenir. Ou nous pourrions faire une pause de quelques minutes et repenser notre stratégie.
Quels problèmes essayons-nous vraiment de résoudre ici?
La gestion de l’état consiste à trouver une bonne approche pour résoudre ces deux problèmes particuliers:
- Comment rendre compréhensibles les flux de données dans une application?
- Comment garder les données d’état toujours synchronisées avec l’interface utilisateur (et vice versa)?
Une fois que vous vous êtes occupé de ceux-ci, tous les autres problèmes que vous pourriez avoir peuvent être déjà résolus ou sont devenus plus faciles à résoudre. Il existe de nombreuses approches possibles pour résoudre ces problèmes, mais nous opterons pour une solution commune qui consiste à centraliser les données et les moyens de les modifier. Les flux de données se dérouleraient comme suit:
Nous ne couvrirons pas ici la partie où les données déclenchent automatiquement la mise à jour de la vue, car elle est liée à des concepts plus avancés de Programmation réactive. C’est un bon sujet de suivi si vous êtes prêt à plonger profondément.
✅ Il existe de nombreuses bibliothèques avec différentes approches de la gestion des États, Redux étant une option populaire. Jetez un coup d’œil aux concepts et aux modèles utilisés, car c’est souvent un bon moyen d’apprendre quels problèmes potentiels vous pouvez rencontrer dans les grandes applications Web et comment ils peuvent être résolus.
Tâche
Nous allons commencer par un peu de refactorisation. Remplacer la déclaration account
:
let account = null;
Par:
let state = {
account: null
};
L’idée est de centraliser toutes nos données d’application dans un seul objet d’état. Nous n’avons que le account
pour l’instant dans l’état, donc cela ne change pas beaucoup, mais cela crée un chemin pour les évolutions.
Nous devons également mettre à jour les fonctions en l’utilisant. Dans les fonctions register()
et login()
, remplacez account = ...
par state.account = ...
;
En haut de la fonction updateDashboard()
, ajoutez cette ligne:
const account = state.account;
Ce refactoring en lui-même n’a pas apporté beaucoup d’améliorations, mais l’idée était de jeter les bases des prochains changements.
Suivre les modifications de données
Maintenant que nous avons mis en place l’objet state
pour stocker nos données, l’étape suivante consiste à centraliser les mises à jour. L’objectif est de faciliter le suivi des changements et de leur moment.
Pour éviter que des modifications soient apportées à l’objet state
, il est également recommandé de le considérer comme immuable, ce qui signifie qu’il ne peut pas être modifié du tout. Cela signifie également que vous devez créer un nouvel objet d’état si vous souhaitez y modifier quoi que ce soit. Ce faisant, vous créez une protection contre les effets secondaires potentiellement indésirables et ouvrez des possibilités de nouvelles fonctionnalités dans votre application, telles que la mise en œuvre de l’annulation/rétablissement, tout en facilitant le débogage. Par exemple, vous pouvez consigner chaque modification apportée à l’état et conserver un historique des modifications pour comprendre la source d’un bogue.
En JavaScript, vous pouvez utiliser Object.freeze()
pour créer une version immuable d’un objet. Si vous essayez d’apporter des modifications à un objet immuable, une exception sera déclenchée.
✅ Connaissez-vous la différence entre un objet immuable peu profond et un objet immuable profond? Vous pouvez en apprendre plus sur ce sujet ici.
Tâche
Créons une nouvelle fonction updateState()
:
function updateState(property, newData) {
state = Object.freeze({
...state,
[property]: newData
});
}
Dans cette fonction, nous créons un nouvel objet d’état et copions les données de l’état précédent à l’aide de l’opérateur spread (...
). Ensuite, nous remplaçons une propriété particulière de l’objet d’état par les nouvelles données en utilisant la notation entre crochets [property]
pour l’affectation. Enfin, nous verrouillons l’objet pour empêcher les modifications en utilisant Object.freeze()
. Nous n’avons que la propriété account
stockée dans l’état pour l’instant, mais avec cette approche, vous pouvez ajouter autant de propriétés que nécessaire dans l’état.
Nous allons également mettre à jour l’initialisation state
pour nous assurer que l’état initial est également gelé:
let state = Object.freeze({
account: null
});
Après cela, mettez à jour la fonction register
en remplaçant l’affectation state.account = result;
par:
updateState('account', result);
Faites de même avec la fonction login
, en remplaçant state.account = data;
par:
updateState('account', data);
Nous allons maintenant saisir l’occasion de résoudre le problème des données de compte qui ne sont pas effacées lorsque l’utilisateur clique sur Logout.
Créez une nouvelle fonction logout()
:
function logout() {
updateState('account', null);
navigate('/login');
}
Dans updateDashboard()
, remplacez la redirection return navigate('/login');
par return logout()
;
Essayez d’enregistrer un nouveau compte, de vous déconnecter et de vous reconnecter pour vérifier que tout fonctionne toujours correctement.
Conseil: vous pouvez jeter un coup d’œil à tous les changements d’état en ajoutant
console.log(state)
au bas deupdateState()
et en ouvrant la console dans les outils de développement de votre navigateur.
Conserver l’état
La plupart des applications Web doivent conserver les données pour pouvoir fonctionner correctement. Toutes les données critiques sont généralement stockées dans une base de données et accessibles via une API de serveur, comme les données du compte utilisateur dans notre cas. Mais parfois, il est également intéressant de conserver certaines données sur l’application cliente qui s’exécute dans votre navigateur, pour une meilleure expérience utilisateur ou pour améliorer les performances de chargement.
Lorsque vous souhaitez conserver des données dans votre navigateur, vous devez vous poser quelques questions importantes:
- Les données sont-elles sensibles? Vous devez éviter de stocker des données sensibles sur le client, telles que les mots de passe des utilisateurs.
- Pendant combien de temps devez-vous conserver ces données? Prévoyez-vous d’accéder à ces données uniquement pour la session en cours ou souhaitez-vous qu’elles soient stockées pour toujours?
Il existe plusieurs façons de stocker des informations dans une application web, en fonction de ce que vous souhaitez réaliser. Par exemple, vous pouvez utiliser les URL pour stocker une requête de recherche et la rendre partageable entre les utilisateurs. Vous pouvez également utiliser des cookies HTTP si les données doivent être partagées avec le serveur, comme les informations d'authentification.
Une autre option consiste à utiliser l’une des nombreuses API de navigateur pour stocker des données. Deux d’entre eux sont particulièrement intéressants:
localStorage
: un Key/Value store permettant de conserver des données spécifiques au site web actuel sur différentes sessions. Les données qui y sont enregistrées n’expirent jamais.sessionStorage
: celui-ci fonctionne de la même manière quelocalStorage
sauf que les données qui y sont stockées sont effacées à la fin de la session (lorsque le navigateur est fermé).
Notez que ces deux API autorisent uniquement le stockage de chaînes. Si vous souhaitez stocker des objets complexes, vous devrez les sérialiser au format JSON à l’aide de JSON.stringify()
.
✅ Si vous souhaitez créer une application web qui ne fonctionne pas avec un serveur, il est également possible de créer une base de données sur le client à l’aide de l’API IndexedDB
. Celui-ci est réservé aux cas d’utilisation avancés ou si vous avez besoin de stocker une quantité importante de données, car il est plus complexe à utiliser.
Tâche
Nous voulons que nos utilisateurs restent connectés jusqu’à ce qu’ils cliquent explicitement sur le bouton Logout, nous utiliserons donc localStorage
pour stocker les données du compte. Tout d’abord, définissons une clé que nous utiliserons pour stocker nos données.
const storageKey = 'savedAccount';
Ajoutez ensuite cette ligne à la fin de la fonction updateState()
:
localStorage.setItem(storageKey, JSON.stringify(state.account));
Avec cela, les données du compte utilisateur seront conservées et toujours à jour car nous avons centralisé précédemment toutes nos mises à jour d’état. C’est là que nous commençons à bénéficier de tous nos refactors précédents 🙂.
Au fur et à mesure que les données sont enregistrées, nous devons également nous occuper de les restaurer lorsque l’application est chargée. Puisque nous allons commencer à avoir plus de code d’initialisation, il peut être judicieux de créer une nouvelle fonction init
, qui inclut également notre code précédent au bas de app.js
:
function init() {
const savedAccount = localStorage.getItem(storageKey);
if (savedAccount) {
updateState('account', JSON.parse(savedAccount));
}
// Notre précédent code d'initialisation
window.onpopstate = () => updateRoute();
updateRoute();
}
init();
Ici, nous récupérons les données enregistrées, et s’il y en a, nous mettons à jour l’état en conséquence. Il est important de le faire avant de mettre à jour l’itinéraire, car il peut y avoir du code s’appuyant sur l’état lors de la mise à jour de la page.
Nous pouvons également faire de la page Dashboard notre page par défaut de l’application, car nous conservons maintenant les données du compte. Si aucune donnée n’est trouvée, le tableau de bord se charge de rediriger vers la page Login de toute façon. Dans updateRoute()
, remplacez le secours return navigate('/login');
par return navigate('/dashboard');
.
Maintenant, connectez-vous à l’application et essayez d’actualiser la page. Vous devez rester sur le tableau de bord. Avec cette mise à jour, nous avons résolu tous nos problèmes initiaux...
Actualiser les données
... Mais nous pourrions aussi en avoir créé un nouveau. Oups!
Accédez au tableau de bord à l’aide du compte test
, puis exécutez cette commande sur un terminal pour créer une nouvelle transaction:
curl --request POST \
--header "Content-Type: application/json" \
--data "{ \"date\": \"2020-07-24\", \"object\": \"Bought book\", \"amount\": -20 }" \
http://localhost:5000/api/accounts/test/transactions
Essayez d’actualiser la page du tableau de bord dans le navigateur maintenant. Que se passe-t-il? Voyez-vous la nouvelle transaction?
L’état est conservé indéfiniment grâce au localStorage
, mais cela signifie également qu’il n’est jamais mis à jour tant que vous ne vous déconnectez pas de l’application et que vous ne vous connectez pas à nouveau!
Une stratégie possible pour résoudre ce problème consiste à recharger les données du compte chaque fois que le tableau de bord est chargé, afin d’éviter les données de blocage.
Tâche
Créez une nouvelle fonction updateAccountData
:
async function updateAccountData() {
const account = state.account;
if (!account) {
return logout();
}
const data = await getAccount(account.user);
if (data.error) {
return logout();
}
updateState('account', data);
}
Cette méthode vérifie que nous sommes actuellement connectés puis recharge les données du compte à partir du serveur.
Créez une autre fonction nommée refresh
:
async function refresh() {
await updateAccountData();
updateDashboard();
}
Celui-ci met à jour les données du compte, puis se charge de mettre à jour le code HTML de la page du tableau de bord. C’est ce que nous devons appeler lorsque l’itinéraire du tableau de bord est chargé. Mettez à jour la définition d’itinéraire avec:
const routes = {
'/login': { templateId: 'login' },
'/dashboard': { templateId: 'dashboard', init: refresh }
};
Essayez de recharger le tableau de bord maintenant, il devrait afficher les données de compte mises à jour.
🚀 Challenge
Maintenant que nous rechargeons les données du compte chaque fois que le tableau de bord est chargé, pensez-vous que nous devons encore conserver toutes les données du compte?
Essayez de travailler ensemble pour modifier ce qui est enregistré et chargé à partir de localStorage
pour n’inclure que ce qui est absolument nécessaire pour que l’application fonctionne.
Quiz de validation des connaissances
Quiz de validation des connaissances
Affectation
Implémenter la boîte de dialogue "Ajouter une transaction"
Voici un exemple de résultat après avoir terminé l’affectation: