Skip to content
Logo
Cover image

React Hooks Personnalisés : useLocalStorage

Posted on

5 min

Après avoir implémenté le Hook useArray pour faciliter la gestion des tableaux, intéressons-nous maintenant au Hook useLocalStorage pour simplifier la gestion du stockage local.

Motivation

Voyons d'abord pourquoi nous voudrions implémenter ce Hook. Imaginons que nous soyons en train de développer une application disposant d'une page de configuration (thème, langue, notifications). Pour sauvegarder la configuration de l'utilisateur, nous utiliserions probablement un objet qui pourrait ressembler à ceci :

1const config = {
2  theme: 'dark',
3  lang: 'fr',
4  notifications: true
5}

Sur la page de configuration, l'interface devra être synchronisée avec l'objet stocké dans le stockage local. Cette page pourrait ressembler à cela :

Aperçu de la page paramètres

Et le code source pourrait être le suivant :

1const defaultConfig = {
2  theme: 'dark',
3  lang: 'fr',
4  notifications: true
5};
6
7const Settings = () => {
8  const [config, setConfig] = useState(() => {
9    const saved = localStorage.getItem('config');
10    if (saved !== null) {
11      return JSON.parse(saved);
12    }
13    return defaultConfig;
14  });
15
16  const handleChange = (e) => {
17    setConfig(oldConfig => {
18      const newConfig = {
19        ...oldConfig,
20        notifications: e.target.checked
21      };
22
23      localStorage.setItem('config', JSON.stringify(newConfig));
24      return newConfig;
25    })
26  }
27
28  return (
29    <>
30      <h1>Settings</h1>
31      <label htmlFor="pushNotifications">
32        Push Notifications
33      </label>
34      <input
35        type="checkbox"
36        id="pushNotifications"
37        checked={config.notifications}
38        onChange={handleChange}
39      />
40    </>
41  );
42};

Comme nous pouvons le constater, cela fait déjà pas mal de code pour seulement activer ou désactiver les notifications. De plus, nous devons nous-même gérer la synchronisation entre l'état de la configuration et le stockage local, ce qui est plutôt encombrant. Un léger manque d'attention pourrait aboutir à une désynchronisation entre ces 2 parties.

Grâce à notre nouveau Hook useLocalStorage, nous allons abstraire de la logique générique dans une fonction séparée afin de réduire la quantité de code nécessaire à cette simple fonctionnalité. De plus, nous n'aurons plus à nous occuper de la synchronisation : c'est le Hook lui-même qui s'en chargera.

Implémentation

Commençons par parler de la signature de ce Hook (quels sont ses paramètres et sa valeur de retour). L'API localStorage nous permet de stocker des données sous forme de clé-valeur.

1// Récupération de la valeur associée à la clé 'config'
2const rawConfig = localStorage.getItem('config');
3
4// Conversion de la configuration brute en objet
5const config = JSON.parse(rawConfig);
6
7// Sauvegarde de la configuration
8localStorage.setItem('config', JSON.stringify(config));

De ce fait, on s'imagine utiliser le Hook de la façon suivante :

1const [config, setConfig] = useLocalStorage('config');

Il va ainsi définir notre variable config à la valeur qu'il trouve dans le stockage local pour la clé 'config'. Si aucune entrée ne correspond à cette clé, config vaudra null.

Nous pourrions également avoir la possibilité de définir une valeur par défaut si la clé donnée n'est pas trouvée. Pour ce faire, il suffit de changer légèrement la signature du Hook pour autoriser un nouveau paramètre optionnel : la valeur par défaut.

1const [config, setConfig] = useLocalStorage('config', defaultConfig);

Nous sommes désormais prêts à implémenter ce Hook. 😎

En premier lieu, nous allons lire dans le stockage local la valeur associée au paramètre key. Si cette clé n'existe pas, nous renverrons alors la valeur par défaut.

1const useLocalStorage = (key, defaultValue = null) => {
2  const [value, setValue] = useState(() => {
3    const saved = localStorage.getItem(key);
4    if (saved !== null) {
5      return JSON.parse(saved);
6    }
7    return defaultValue;
8  });
9};

Nous venons de passer la première étape de l'implémentation. Attention cependant à gérer le cas où la méthode JSON.parse lèverait une exception en renvoyant la valeur par défaut.

1const useLocalStorage = (key, defaultValue = null) => {
2  const [value, setValue] = useState(() => {
3      const saved = localStorage.getItem(key);
4      if (saved !== null) {
5        try {
6          return JSON.parse(saved);
7        } catch {
8          return defaultValue;
9        }
10      }
11      return defaultValue;
12  });
13};

Voilà qui est mieux. Il ne nous reste plus qu'à écouter les changements de la variable value pour mettre à jour en conséquence le stockage local. Pour ce faire, nous allons utiliser le Hook useEffect.

1const useLocalStorage = (key, defaultValue = null) => {
2  const [value, setValue] = useState(/* ... */);
3
4  useEffect(() => {
5    const rawValue = JSON.stringify(value);
6    localStorage.setItem(key, rawValue);
7  }, [value]);
8};

⚠️ La méthode JSON.stringify peut également lancer des erreurs.

Ça y est, on a fini ? Pas vraiment. Tout d'abord, nous n'avons pas retourné value ni son setter.

1const useLocalStorage = (key, defaultValue = null) => {
2  const [value, setValue] = useState(/* ... */);
3
4  useEffect(/* ... */);
5
6  return [value, setValue];
7};

De plus, n'oublions pas que la valeur du paramètre key peut également changer. Dans notre exemple, la clé fournie est une constante, mais cela aurait très bien pu être une valeur issue d'un appel à useState, auquel cas cette dernière peut varier. Corrigeons ce petit problème.

1const useLocalStorage = (key, defaultValue = null) => {
2  const [value, setValue] = useState(/* ... */);
3  const [oldKey, setOldKey] = useState(key)
4
5  useEffect(() => {
6    const rawValue = JSON.stringify(value);
7    localStorage.setItem(key, rawValue);
8    localStorage.removeItem(oldKey);
9    setOldKey(key);
10  }, [key, value]);
11
12  return [value, setValue];
13};

Nous faisons attention à supprimer du stockage local la clé précédente et sa valeur associée afin de ne pas le surcharger.

Nous en avons maintenant terminé avec l'implémentation. Vous avez cependant toujours la possibilité de l'adapter à vos besoins. Par exemple, si vous souhaitez plutôt utiliser le stockage de session, il suffit de remplacer localStorage par sessionStorage. On pourrait également ajouter une méthode clear pour supprimer la clé du stockage ainsi que sa valeur. En bref, les possibilités sont infinies, et je vous donne des idées d'améliorations dans quelques instants.

Utilisation

Nous pouvons maintenant simplifier notre page de configuration en utilisant notre tout nouveau Hook. Désormais, nous n'avons plus à gérer la synchronisation. Voici le résultat final.

1const defaultConfig = {
2  theme: "light",
3  lang: "fr",
4  notifications: true
5};
6
7const Settings = () => {
8  const [config, setConfig] = useLocalStorage("config", defaultConfig);
9
10  const handleChange = (e) => {
11    setConfig(oldConfig => ({
12      ...oldConfig,
13      notifications: e.target.checked
14    }));
15  };
16
17  return (
18    <>
19      <h1>Settings</h1>
20
21      <label htmlFor="pushNotifications">Push Notifications</label>
22      <input
23        type="checkbox"
24        id="pushNotifications"
25        checked={config.notifications}
26        onChange={handleChange}
27      />
28    </>
29  );
30};

Idées d'Améliorations

  • Gérer les éventuelles erreurs lancées par JSON.stringify
  • Si la valeur devient null, nettoyer le stockage local associé à cette clé (via localStorage.removeItem)
  • Créer un Hook générique useStorage qui prend en paramètre le stockage à utiliser (localStorage ou sessionStorage)

Conclusion

Une fois de plus, nous avons radicalement simplifié notre code en définissant un Hook personnalisé. Cependant, notre implémentation n'est pas limitée à ce que nous avons actuellement : nous pouvons la personnaliser en fonction de nos besoins afin d'en tirer un maximum de profit. Dans le prochain épisode, nous allons nous intéresser à un autre Hook particulièrement utile : useNetworkState.


Code source disponible sur CodeSandbox.

Did you like this article?

Feel free to share it on social media! 😊

Copyright © 2022 Ludovic CHOMBEAU