8
Module 8
Notion de contexte
A la fin de ce module, l'étudiant devra être capable d'Effectuer le partage global des données dans un projet React avec Next.js.

Contexte
Jusqu'à présent, dans notre application 'biblio-app', nous avons eu l'habitude de faire passer les données de haut en bas (du parent au fils) via les props. Cette approche peut devenir inefficace pour certains types de props tels que le thème de l'interface utilisateur et la langue préférée. En effet, ces props seront nécessaires pour de nombreux pour ne pas dire presque tous les composants de notre application. Le Contexte nous permettra de résoudre ce problème en partageant des données entre tous les composants de l'application sans avoir à les passer explicitement à chaque niveau de l'arborescence.
Illustration
Pour illustrer cela, nous allons ajouter à notre composant 'Header', une case à cocher pour changer le thème de l'interface graphique de notre application (light mode et dark mode). Pour cela, les étapes à suivre :
Créer un composant 'ThemeProvider'
/components/ThemeProvider.jsx
'use client'
import { createContext, useContext, useState } from 'react' 
// Crée un contexte pour stocker le thème.
const ThemeContext = createContext(); 
// Définit un composant React appelé ThemeProvider pour fournir le thème à ses enfants.
export function ThemeProvider({children}) { 
    const [theme, setTheme] = useState('light'); 
    // Rend le contexte ThemeContext disponible pour ses enfants avec la valeur [theme, setTheme].
    return <ThemeContext.Provider value={[theme, setTheme]> {/* Rend les composants enfants du ThemeProvider. */}
        {children} 
    </ThemeContext.Provider> 
}

// Définit un hook personnalisé useTheme pour utiliser le thème actuel.
export function useTheme() { 
    // Utilise le hook useContext pour accéder au thème actuel et à la fonction pour le modifier.
    const [theme, setTheme] = useContext(ThemeContext);
    // Retourne le thème actuel et la fonction pour le modifier. 
    return [theme, setTheme]; 
}
Voici une explication globale de ce que fait ce composant 'ThemeProvider':
Création du contexte de thème (ThemeContext) : La ligne const ThemeContext = createContext(); crée un contexte React qui sera utilisé pour partager le thème avec les composants de l'application.
Fourniture du thème aux composants (ThemeProvider) : Le composant ThemeProvider est créé pour encapsuler d'autres composants et leur fournir le thème. Il utilise le hook useState pour gérer l'état du thème. Tous les composants enfants de ThemeProvider pourront accéder au thème via le contexte ThemeContext.
Utilisation du thème dans les composants (useTheme) : Le hook personnalisé useTheme est créé pour permettre aux composants de récupérer le thème actuel et de le modifier si nécessaire. Il utilise le hook useContext pour accéder au contexte ThemeContext, ce qui lui permet de récupérer le thème actuel et la fonction pour le modifier.
En résumé, ce composant 'ThemeProvider' permettra la gestion centraliée du thème pour notre application. Le composant ThemeProvider fournit le thème à tous ses enfants, tandis que le hook useTheme permet à ces enfants de récupérer et de modifier le thème en fonction de leurs besoins.
Modifier le composant 'MenuNav'
/components/MenuNav.jsx (nouveau)
'use client'
import Link from 'next/link';
import styles from './MenuNav.module.css';
import { useTheme } from './ThemeProvider'; //Importer le hook personnalisé
export default function MenuNav() {
    const [theme, setTheme] = useTheme();
    const handleTheme = () => {
        if (theme === 'light') {
            setTheme('dark');
        }
        else {
            setTheme('light');
        }
    }
    return <nav className={styles.nav}>
        <ul>
            <li><Link href="/">Accueil</Link></li>
            <li><Link href="/documents">Documents</Link></li>
            <li><Link href="/contact">Contact</Link></li>
            <li><Link href="/connexion">Connexion</Link></li>
        </ul>
        <label>
            Mode Sombre:
            <input
                type="checkbox"
                checked={theme === 'dark'}
                onChange={handleTheme}
            />
        </label>
    </nav>
}
Dans le composant 'MenuNav', nous avons importé le hook personnalisé 'useTheme' et ce dernier nous permet de basculer la variable 'theme' entre les deux états 'light' et 'dark', à chaque fois que la case à cocher est cochée ou décochée.
Remarque
Il est important de ne pas oublier 'use client' se trouvant en debut de fichier.
Modifier le layout de base
Il faut importer le ThemeProvider dans le fichier 'layout.jsx' avec la commande suivante :
/app/layout.jsx
import { ThemeProvider } from '@/components/ThemeProvider';
Par la suite, il faut transferer le ThemeProvider à tous les enfants en mettant tout le contenu du 'body' entre les balises <ThemeProvider> et </ThemeProvider>. Dans notre cas, on aura le code suivant :
/app/layout.jsx
<body className={inter.className + ' ' + styles.body}>
 <ThemeProvider>
    <Header />
    <main className={styles.main}>
      {children}
    </main>
    <Footer />
 </ThemeProvider>
</body>
Modifier le composant 'Header'
/components/Header.jsx (nouveau)
import Image from 'next/image';
    import logo from '@/public/react.webp';
    import MenuNav from './MenuNav';
    import { useTheme } from './ThemeProvider'; //Importer le hook personnalisé pour manipuler le thème
    export default function Header() {
        const [theme, setTheme] = useTheme();
        return <header className={styles.header +' '+
        (theme === 'light'? styles.light : styles.dark)
        }>
            <div className={styles.title}>
                <Image
                    src={logo}
                    alt="Logo React"
                    width={80}
                />
                <h1>Biblio App</h1>
            </div>
            <MenuNav/>
        </header>
    }
Modifier le composant 'Header.module.css'
/components/Header.module.css (ajouter)
.light{
    background-color: var(--first-color);
}
.dark{
    background-color: #000;
}
Résultat
Après toutes ces modifications, si on coche la case du mode sombre, l'arrière plan du Header doit changer.

Local Storage
Si nous actualisons notre navigateur, nous constaterons que le thème n'est pas conservé. Pour résoudre ce problème, nous allons sauvegarder le thème dans un espace de stockage du navigateur appelé Local storage ou stockage Local. Cela se fera en modifiant légèrement le composant 'ThemeProvider'.
/components/ThemeProvider.jsx (nouveau)
'use client'
import { createContext, useContext, useEffect, useState } from 'react'
// Crée un contexte pour stocker le thème.
const ThemeContext = createContext();
// Définit un composant React appelé ThemeProvider pour fournir le thème à ses enfants.
export function ThemeProvider({ children }) {
    const [theme, setTheme] = useState('light');
    useEffect(() => {
        setTheme(localStorage.getItem('theme') || 'light');
    }, [setTheme]);
    // Rend le contexte ThemeContext disponible pour ses enfants avec la valeur [theme, setTheme].
    return <ThemeContext.Provider value={[theme, setTheme]}> {/* Rend les composants enfants du ThemeProvider. */}
        {children}
    </ThemeContext.Provider>
}
// Définit un hook personnalisé useTheme pour utiliser le thème actuel.
export function useTheme() {
    // Utilise le hook useContext pour accéder au thème actuel et à la fonction pour le modifier.
    const [theme, setTheme] = useContext(ThemeContext);
    const setThemeWithStorage = (theme) => {
        localStorage.setItem('theme', theme);
        setTheme(theme);
    }
    // Retourne le thème actuel et la fonction pour le modifier. 
    return [theme, setThemeWithStorage];
}    

Hooks personnalisées
Jusqu'à présent à l'exception de l'exemple précédent, nous avons eu l'habitude d'utiliser des hooks déjà intégrés à React. Mais nous pouvons créer les notres. Pour illustrer cela, nous allons revenir sur notre composant 'FormControlle'.
Dans ce composant, nous avons créé deux champs de type « input » (à savoir le nom et courriel). Pour chacun de ces champs, nous avons créé une variable en utilisant le hook 'useState'. Enfin, toujours pour chacun de ces champs, nous avons créé une fonction 'handleNomDuChamp' pour faire sa mise à jour. On peut constater qu'il y a beaucoup de répétitions qui sont faciles à gérer pour l'instant puisque nous avons juste deux champs. Si l'on se retrouve avec un peu plus de champs, cette approche deviendra lourde.
Pour résoudre ce problème, nous allons définir un hook nommé 'useFormInput', qui réalisera toutes les actions citées à chaque fois que nous créerons un champ de type « input ». Cela rendra le code de notre composant beaucoup plus agréable.
/components/useFormInput.jsx
import { useState } from 'react';
export function useFormInput(valeurInitiale) {
  const [value, setValue] = useState(valeurInitiale);
  function handleChange(e) {
    setValue(e.target.value);
  }
  const inputProps = {
    value: value,
    onChange: handleChange
  };
  return inputProps;
}   
/components/FormControlle.jsx (nouveau)
'use client'
import styles from './Form.module.css';
import { useFormInput } from './useFormInput';
export default function FormControlle() {
    const nom = useFormInput('');
    const courriel = useFormInput('');
    const handleSubmit = (event) => {
        event.preventDefault(); // Empêche la soumission par défaut du formulaire
        console.log(`Nom: ${nom.value}, Courriel: ${courriel.value}`); //Affichage du nom et courriel
    };
    return <>
        <form onSubmit={handleSubmit} className={styles.form}>
            <div>
                <label>Nom:</label>
                <input {...nom} />
            </div>
            <div>
                <label>Email:</label>
                <input {...courriel} />
            </div>
            <button type="submit">Envoyer</button>
        </form>
    </>;
}

Internationalisation (i18n)
L'internationalisation (i18n) est le processus de conception et de développement d'une application pour la rendre accessible à un public international, en prenant en compte les différences linguistiques et culturelles. Pour illustrer cela, nous allons transformer notre application 'biblio-app' en une application bilingue(Français et Anglais).
Plusieurs bibliothèques JavaScript populaires peuvent être utilisées pour la gestion de l'internationalisation dans les applications React. Parmi ces bibliothèques, on peut citer react-i18next, react-intl, et linguiJS. Nous utiliserons react-intl dans notre application.
Voici les étapes à suivre :
Installer React-intl
Dans un terminal de l'application, utiliser la commande suivante pour installer la bibliothèque react-intl
Terminal
npm i -S react-intl
Les données de chaque langue
Dans un premier temps, pour chaque langue, nous allons créer un fichier json contenant les données du site équivalentes à cette dernière. Ces fichiers serons enregistrés dans un dossier nommé 'i18n' créé à la racine du dossier l'application.
/i18n/fr.json
{
    "app.header.title": "Biblio App",
    "app.header.menu.lien1": "Accueil",
    "app.header.menu.lien2": "Documents",
    "app.header.menu.lien3": "Contact",
    "app.header.menu.lien4": "Connexion",
    "app.header.theme": "Mode sombre"
}
/i18n/en.json
{
    "app.header.title": "Biblio App",
    "app.header.menu.lien1": "Home",
    "app.header.menu.lien2": "Documents",
    "app.header.menu.lien3": "Contact",
    "app.header.menu.lien4": "Log In",
    "app.header.theme": "Dark mode"
}
LocaleProvider
Nous allons créer un Provider qui nous permettra de partager la langue courante ansi que les données correspondantes.
/components/LocaleProvider.jsx
'use client'
import { createContext, useContext, useState } from 'react'
import { IntlProvider } from 'react-intl'
import englishData from '@/i18n/en.json' // Importation des données de traduction anglaises
import frenchData from '@/i18n/fr.json' // Importation des données de traduction françaises
// Création d'un objet contenant les données de traduction pour chaque langue supportée
const translations = {
    en: englishData,
    fr: frenchData
}
// Création du contexte de localisation
const LocaleContext = createContext()
// Component pour fournir la localisation à l'application
export function LocaleProvider({ children }) {
    // State pour gérer la langue sélectionnée
    const [locale, setLocale] = useState('fr');
    // Rendu du contexte de localisation et du composant IntlProvider de react-intl
    return (
        <LocaleContext.Provider value={[locale, setLocale]}>
            {/*IntlProvider avec la langue actuelle et les traductions correspondantes */}
            <IntlProvider locale={locale} messages={translations[locale]}>
                {/* Rendu des enfants de LocaleProvider */}
                {children}
            </IntlProvider>
        </LocaleContext.Provider>
    );
}
// Hook personnalisé pour accéder à la localisation
export function useLocale() {
    const [locale, setLocale] = useContext(LocaleContext);
    return [locale, setLocale];
}
Modifier le layout de base
Il faut importer le LocaleProvider dans le fichier 'layout.jsx' avec la commande suivante :
/app/layout.jsx
import { LocaleProvider } from '@/components/LocaleProvider';
Par la suite, il faut transferer le LocaleProvider à tous les enfants en mettant tout le contenu du 'body' entre les balises <LocaleProvider> et </LocaleProvider>. Dans notre cas, on aura le code suivant :
/app/layout.jsx
<body className={inter.className + ' ' + styles.body}>
 <LocaleProvider>
   <ThemeProvider>
    <Header />
    <main className={styles.main}>
      {children}
    </main>
    <Footer />
   </ThemeProvider>
  <LocaleProvider>
</body>
Modifier le composant 'MenuNav'
/components/MenuNav.jsx
'use client'
import Link from 'next/link';
import styles from './MenuNav.module.css';
import { useTheme } from './ThemeProvider'; //Importer le hook personnalisé pour manipuler le thème
import { useLocale } from './LocaleProvider';
import { FormattedMessage } from 'react-intl'
export default function MenuNav() {
    const [theme, setTheme] = useTheme();
    const handleTheme = () => {
        if (theme === 'light') {
            setTheme('dark');
        }
        else {
            setTheme('light');
        }
    }
    const [locale, setLocale] = useLocale();
    const handleLocale = () => {
        if (locale === 'fr') {
            setLocale('en');
        }
        else {
            setLocale('fr');
        }
    }
    return <nav className={styles.nav}>
        <ul>
            <li><Link href="/"><FormattedMessage id='app.header.menu.lien1'/></Link></li>
            <li><Link href="/documents"><FormattedMessage id='app.header.menu.lien2'/></Link></li>
            <li><Link href="/contact"><FormattedMessage id='app.header.menu.lien3'/></Link></li>
            <li><Link href="/connexion"><FormattedMessage id='app.header.menu.lien4'/></Link></li>
        </ul>
        <label>
            Mode Sombre:
            <input
                type="checkbox"
                checked={theme === 'dark'}
                onChange={handleTheme}
            />
        </label>
        <div>
            <label>
                EN :
                <input
                    type="checkbox"
                    checked={locale === 'en'}
                    onChange={handleLocale} />
            </label>
        </div>
    </nav>
}
Plusieurs changements ont été réalisés dans le composant 'MenuNav.jsx' :
useLocale : useLocale a été importé pour gérer la langue courante
FormattedMessage : Le composant 'FormattedMessage' de 'react-intl' a été importé également. Son rôle est d'intégrer les données de notre application de manière dynamique et de gérer les traductions de ces données.
Remarque
On peut constater que le mot 'Accueil' a été remplacé par '<FormattedMessage id='app.header.menu.lien1'/>'. Ce code permettra d'afficher soit 'Accueil' si la langue courante est 'fr', soit 'Home' si la langue courante est 'en'. Il en est de même pour les autres liens.
Travail à faire
Vous avez probablement constaté que lorsque le navigateur est actualisé, la version anglaise de notre application n'est pas maintenue. Votre travail consistera à résoudre ce problème comme nous l'avons fait précédemment en utilisant le 'Local Storage'.

Code source
Important
Avant d'executer la commande 'npm run dev', bien vouloir exécuter la commande 'npm i'.

Travail à faire