# React Hooks Personnalisés : useLocalStorage

Après avoir implémenté le Hook [useArray](https://blog.iamludal.fr/react-hooks-personnalises-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 :

```jsx
const config = {
  theme: 'dark',
  lang: 'fr',
  notifications: true
}
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bpr40cqmb5p2ksk69ll8.png)

Et le code source pourrait être le suivant :

```jsx
const defaultConfig = {
  theme: 'dark',
  lang: 'fr',
  notifications: true
};

const Settings = () => {
  const [config, setConfig] = useState(() => {
    const saved = localStorage.getItem('config');
    if (saved !== null) {
      return JSON.parse(saved);
    }
    return defaultConfig;
  });

  const handleChange = (e) => {
    setConfig(oldConfig => {
      const newConfig = {
        ...oldConfig,
        notifications: e.target.checked
      };

      localStorage.setItem('config', JSON.stringify(newConfig));
      return newConfig;
    })
  }

  return (
    <>
      <h1>Settings</h1>
      <label htmlFor="pushNotifications">
        Push Notifications
      </label>
      <input
        type="checkbox"
        id="pushNotifications"
        checked={config.notifications}
        onChange={handleChange}
      />
    </>
  );
};
```

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.

```jsx
// Récupération de la valeur associée à la clé 'config'
const rawConfig = localStorage.getItem('config');

// Conversion de la configuration brute en objet
const config = JSON.parse(rawConfig);

// Sauvegarde de la configuration
localStorage.setItem('config', JSON.stringify(config));
```

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

```jsx
const [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.

```jsx
const [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.

```jsx
const useLocalStorage = (key, defaultValue = null) => {
  const [value, setValue] = useState(() => {
    const saved = localStorage.getItem(key);
    if (saved !== null) {
      return JSON.parse(saved);
    }
    return defaultValue;
  });
};
```

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.

```jsx
const useLocalStorage = (key, defaultValue = null) => {
  const [value, setValue] = useState(() => {
      const saved = localStorage.getItem(key);
      if (saved !== null) {
        try {
          return JSON.parse(saved);
        } catch {
          return defaultValue;
        }
      }
      return defaultValue;
  });
};
```

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`.

```jsx
const useLocalStorage = (key, defaultValue = null) => {
  const [value, setValue] = useState(/* ... */);

  useEffect(() => {
    const rawValue = JSON.stringify(value);
    localStorage.setItem(key, rawValue);
  }, [value]);
};
```

> ⚠️ 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.

```jsx
const useLocalStorage = (key, defaultValue = null) => {
  const [value, setValue] = useState(/* ... */);

  useEffect(/* ... */);

  return [value, setValue];
};
```

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.

```jsx
const useLocalStorage = (key, defaultValue = null) => {
  const [value, setValue] = useState(/* ... */);
  const [oldKey, setOldKey] = useState(key)

  useEffect(() => {
    const rawValue = JSON.stringify(value);
    localStorage.setItem(key, rawValue);
    localStorage.removeItem(oldKey);
    setOldKey(key);
  }, [key, value]);

  return [value, setValue];
};
```

>  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.

```jsx
const defaultConfig = {
  theme: "light",
  lang: "fr",
  notifications: true
};

const Settings = () => {
  const [config, setConfig] = useLocalStorage("config", defaultConfig);

  const handleChange = (e) => {
    setConfig(oldConfig => ({
      ...oldConfig,
      notifications: e.target.checked
    }));
  };

  return (
    <>
      <h1>Settings</h1>

      <label htmlFor="pushNotifications">Push Notifications</label>
      <input
        type="checkbox"
        id="pushNotifications"
        checked={config.notifications}
        onChange={handleChange}
      />
    </>
  );
};
```

## 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](https://codesandbox.io/s/custom-react-hooks-uselocalstorage-0pdve?file=/src/App.tsx).

