Hook
Les hooks sont une fonctionnalité introduite par React
pour permettre aux composants d'accéder à l'état local et à d'autres fonctionnalités de React qui n'étaient auparavant disponibles que dans les composants de classe.
Les hooks sont des fonctions qui vous permettent d'utiliser l'état local et d'autres fonctionnalités de React dans les composants. Les plus couramment utilisés sont : useState
, useEffect
, et useContext
.
useState
Le hook useState
permet à un composant fonctionnel de déclarer un état local. Il prend un argument initial et renvoie un tableau contenant deux éléments : la valeur actuelle de l'état et une fonction pour la mettre à jour.
import React, { useState } from "react";
const Compteur: React.FC =() => {
// Déclare une variable d'état appelée "count" avec une valeur initiale de 0
const [count, setCount] = useState<number>(0);
return (
<div>
<p>Le compteur est à : {count}</p>
{/* Utilise la fonction setCount pour mettre à jour la valeur de count */}
<button onClick={() => setCount(count + 1)}>
Incrémenter
</button>
</div>
);
}
export default Compteur;
Dans cet exemple, count
est un état local à Compteur
, et setCount
est une fonction que vous pouvez utiliser pour mettre à jour cet état.
L'utilisation d'un état local permet à un composant de gérer son propre état interne sans avoir besoin de dépendre de l'état d'autres composants. Cela favorise la modularité et la réutilisabilité des composants, car chacun peut gérer son propre état de manière indépendante.
Dans le cas de l'utilisation de useState
, vous ne devez pas modifier la valeur de count
directement comme vous le feriez avec une variable classique. Au lieu de cela, il faut utiliser la fonction setCount
fournie par useState
pour mettre à jour la valeur de count
. Cela garantit que React est informé des changements d'état et peut gérer correctement le rendu du composant.
Si vous faites quelque chose comme count = count + 1
, React ne sera pas conscient du changement, et cela pourrait entraîner un comportement inattendu ou des problèmes de rendu.
useEffect
Le hook useEffect
est utilisé pour effectuer des effets de bord dans un composant fonctionnel. Il prend une fonction en tant qu'argument, qui sera exécutée après chaque rendu du composant.
C’est ici que vous devriez placer les initialisations qui requièrent l’existence de nœuds du DOM. Si vous avez besoin de charger des données depuis un point d’accès distant, c’est aussi le bon endroit pour déclencher votre requête réseau.
useEffect(() => {
//du code
}, []);
//donner le tableau des dépendances
//ici il est vide, cela veut dire que ce useEffect ne s'exécutera qu'une et une seule fois
//lorsque le composant est monté pour le première fois.
const [count, setCount] = useState<number>(0);
const [connected, setConnected] = useState<boolean>(true);
useEffect(() => {
//faire le code entre les accolades si l'état de count ou de connected est modifié à un autre endroit du composant
}, [count,connected]);
Il n'est évidement pas possible d'écrire le code suivant, sinon il y aurait une infinité d'appels à ce useEffect
.
const [count, setCount] = useState<number>(0);
const [connected, setConnected] = useState<boolean>(true);
useEffect(() => {
setCount(3);
}, [count,connected]);
Ci-dessous un exemple qui effectue un seul fetch
au montage du composant et met à jour le jsx
.
import React, { useEffect, useState } from "react";
const Lorem: React.FC =() => {
const [data, setData] = useState<string | null>(null);
// Utilise useEffect pour effectuer une action après chaque rendu
useEffect(() => {
// Exécute une requête Fetch pour obtenir des données depuis une API
fetch('https://jsonplaceholder.typicode.com/todos/2')
.then(response => response.json())
.then(json => setData(json.title))
}, []); // Le tableau vide signifie que cet effet est exécuté une seule fois après le montage
return (
<div>
<p>Donnée chargée lorsque vous avez ouvert cette page : {data}</p>
</div>
);
}
export default Lorem;
Dans l'exemple ci-dessous, à chaque clic sur le bouton, count
est mis à jour, le useEffect
est donc appelé. Ce dernier fait une requête fetch
qui met à jour la variable data
.
import React, { useEffect, useState } from "react";
const LoremParametrable: React.FC =() => {
const [count, setCount] = useState<number>(0);
const [data, setData] = useState<string | null>(null);
// Utilise useEffect pour effectuer une action après chaque rendu
useEffect(() => {
//le if est obligatoire, sinon au montage du composant,
//le use effect est exécuté avec count = 0
//et une requète https://jsonplaceholder.typicode.com/todos/0
//est exécutée (alors qu'on souhaite le faire uniquement à partir de 1)
if (count !== 0) {
fetch(`https://jsonplaceholder.typicode.com/todos/${count}`)
.then(response => response.json())
.then(json => setData(json.title))
}
// Notez que nous avons ajouté count au tableau des dépendances
// Cela signifie que useEffect sera réexécuté chaque fois que count change
}, [count]);
return (
<div>
<button onClick={() => setCount(count + 1)}>Faire une requête</button>
<p>Nombre de requêtes effectuées : {count}</p>
<p>Résulat de la requête : {data}</p>
</div>
);
}
export default LoremParametrable;
Ci dessous un exemple très proche du précédent. Il permet d'afficher toutes les réponses obtenues du serveur.
import React, { useEffect, useState } from "react";
const LoremFinal: React.FC =() => {
const [count, setCount] = useState<number>(0);
const [dataList, setDataList] = useState<string[]>([]);
useEffect(() => {
if (count !== 0) {
fetch(`https://jsonplaceholder.typicode.com/todos/${count}`)
.then(response => response.json())
.then(json => {
// Ajoutez la nouvelle réponse à la liste existante
setDataList(prevDataList => [...prevDataList, json.title]);
});
}
}, [count]);
return (
<div>
<button onClick={() => setCount(count + 1)}>Faire une requête</button>
<p>Nombre de requêtes effectuées : {count}</p>
<p>Résultats de la requête :</p>
{dataList.length > 0 && (
<>
<ul>
{dataList.map((data, index) => (
<li key={index}>{data}</li>
))}
</ul>
</>
)}
</div>
);
}
export default LoremFinal;
Je souhaite attirer votre attention sur la ligne de code setDataList(prevDataList => [...prevDataList, json.title]);
L'idée de cette ligne est de ne pas faire une mise à jour directe de dataList
dans React car l'état suivant de dataList
dépend du précédent.
Voici deux raisons dans le détail :
- Assurer la Cohérence de l'État : React n'applique pas immédiatement les mises à jour d'état. Si vous faites une mise à jour directe comme :
setDataList([...dataList, json.title]);
il peut arriver que dataList
ne soit pas à jour au moment de l'exécution, surtout si plusieurs mises à jour d'état sont déclenchées rapidement. Cela peut entraîner des problèmes, comme des données manquantes ou des doublons.
- En revanche, en utilisant une fonction de mise à jour basée sur l'état précédent :
setDataList(prevDataList => [...prevDataList, json.title]);
vous êtes sûr que la mise à jour utilise toujours la version la plus récente de dataList
, quelle que soit la rapidité ou la fréquence des mises à jour.
Quand vous accédez directement à dataList
, vous dépendez de la valeur qui est dans la mémoire locale au moment où l'effet est exécuté. Mais dans React, les mises à jour d'état sont asynchrones. Cela signifie que plusieurs mises à jour d'état peuvent être en attente, ce qui peut provoquer des collisions ou écraser des données.
Exemple problématique :
- Vous avez
dataList = ["A"]
. - Deux appels simultanés à
setDataList([...dataList, "B"])
etsetDataList([...dataList, "C"])
. - Résultat attendu :
["A", "B", "C"]
. Résultat réel : soit["A", "B"]
, soit["A", "C"]
.
En utilisant une fonction de mise à jour basée sur l'état précédent, React gère correctement chaque mise à jour en file d'attente, en garantissant que les modifications successives s'appliquent à la version la plus récente de l'état.
Exemple Comparatif
Mauvaise Méthode (Mise à jour directe)
useEffect(() => {
fetchData().then(json => {
setDataList([...dataList, json.title]); // Risque : dataList pourrait être obsolète ici
});
}, []);
Bonne Méthode (Basée sur l'état précédent)
useEffect(() => {
fetchData().then(json => {
setDataList(prevDataList => [...prevDataList, json.title]); // Toujours fiable
});
}, []);
Dans le second cas, vous êtes sûr que chaque mise à jour est effectuée correctement, même si des appels multiples à setDataList
sont effectués.
Est-ce obligatoire de toujours utiliser cette manière de mettre à jour une donnée ?
Evidemment non. Il faut le faire uniquement si la mise à jour nécessite de connaitre l'état précédent.
Dans les exemples ci-dessous, un simple appel au set
suffit car il n'y a pas de lien avec l'état précédent.
// Nouvelles valeurs sans tenir compte de l'ancien état
useEffect(() => {
setDataList(["Nouvelle liste"]);
setCount(5);
setValue("nouvelle valeur");
}, []);
set
Ci-dessous je vous déroule un exemple pour vous expliquer pourquoi il ne faut pas enchainer les set
si l'un d'entre eux dépend d'un set
précédent.
Il faut comprendre que les appels à set
sont asnychrones. Donc aucune garantie que setValeurA(10);
soit terminé quand setValeurB(valeurA + 5);
s'exécute.
import React, { useState, useEffect } from "react";
const ExempleMauvaisePratique: React.FC = () => {
const [valeurA, setValeurA] = useState(0);
const [valeurB, setValeurB] = useState(0);
useEffect(() => {
// Simulation d'une mise à jour
setValeurA(10);
// MAUVAISE PRATIQUE : On suppose que valeurA est déjà à jour ici.
setValeurB(valeurA + 5);
}, []);
return (
<div>
<p>Valeur A: {valeurA}</p>
<p>Valeur B: {valeurB}</p>
</div>
);
}
La bonne pratique est d'utiliser un useEffect
dédié à la mise à jour de la valeurB
.
import React, { useState, useEffect } from "react";
const ExempleBonnePratique: React.FC = () => {
const [valeurA, setValeurA] = useState(0);
const [valeurB, setValeurB] = useState(0);
useEffect(() => {
// Mise à jour initiale
setValeurA(10);
}, []);
useEffect(() => {
// Mise à jour de B en réaction au changement de A
setValeurB(valeurA + 5);
}, [valeurA]); // Déclenché chaque fois que valeurA change
return (
<div>
<p>Valeur A: {valeurA}</p>
<p>Valeur B: {valeurB}</p>
</div>
);
}
useContext
useContext
est un hook de React qui permet à un composant de consommer une valeur fournie par un contexte en React. Les contextes sont utilisés pour partager des valeurs telles que des thèmes, des préférences d'utilisateur ou toute autre information globale à travers l'arbre des composants sans avoir à passer explicitement les données à travers chaque niveau (vers le haut et vers le bas).
Voici quelques exemples pour illustrer l'intérêt de useContext
:
Supposons que vous avez un thème pour votre application (par exemple, mode sombre ou mode clair). Vous pouvez créer un contexte pour gérer le thème et utiliser useContext
pour récupérer la valeur du thème dans les composants qui en ont besoin.
import React, { createContext, useContext, useState, ReactNode } from 'react';
//typage des données qui seront associées au contexte
type Theme = 'clair' | 'sombre';
type ThemeContextType = {
theme: Theme;
basculerTheme: () => void;
}
// Lorsqu'un createContext est déclaré, il faut fournir une valeur initiale.
// Ici, la valeur initiale n'existe pas encore avant que le contexte ne soit utilisé dans un ThemeProvider.
//undefined indique que si un composant essaie d'utiliser le contexte en dehors d'un ThemeProvider,
// il n'obtiendra pas de valeur valide.
const ThemeContexte = createContext<ThemeContextType | undefined>(undefined);
//ypage de la props qu'il faut passer au composant
type ThemeProviderProps = {
children: ReactNode; // Décrit les enfants React passés au composant
//ses enfants auront accès au contenu de la variable theme
//et à la fonction basculerTheme
}
//on construit un composant qui sera utilisé comme
//un fournisseur d'un contexte (une valeur et une méthode)
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
const [theme, setTheme] = useState<Theme>('clair');
//on fait bien attention ici à prendre en compte l'état précédent de theme
//pas de useEffect ici, ce sont les composants enfants qui appelleront cette fonction
const basculerTheme = () => {
setTheme((prevTheme) => (prevTheme === 'clair' ? 'sombre' : 'clair'));
};
useEffect(() => {
document.body.className = theme === 'clair' ? 'theme-clair' : 'theme-sombre'; // Applique la classe sur le body
}, [theme]); // Se déclenche à chaque changement du thème (le changement sera demandé par les composants enfants).
return (
// Fournit un contexte aux composants imbriqués (les "enfants")
<ThemeContexte.Provider value={{ theme, basculerTheme }}>
{children}
</ThemeContexte.Provider>
);
{/*
Tout ce qui est passé en tant qu'enfant de ce `ThemeProvider`
(c'est-à-dire tout ce qui est inclus entre `<ThemeProvider>` et `</ThemeProvider>` dans l'arborescence de composants)
sera rendu ici.
Exemple :
<ThemeProvider>
<App />
</ThemeProvider>
Dans ce cas, `<App />` sera passé à `enfants` et rendu ici.
*/}
};
// Création d'un hook personnalisé `useTheme`
//il permet d'accéder aux valeurs du contexte `ThemeContexte`.
//Cette structure garantit que les composants qui utilisent le hook
// `useTheme` ne peuvent accéder au contexte que s'ils sont enveloppés par un `ThemeProvider`.
export const useTheme = (): ThemeContextType => {
// Utilise le hook React `useContext` pour accéder au contexte `ThemeContexte`.
// `useContext` récupère la valeur actuelle du contexte, qui doit être fournie par un `ThemeContexte.Provider`.
const context = useContext(ThemeContexte);
// Si `context` est `undefined` (ce qui signifie que `useTheme` est utilisé en dehors d'un `ThemeProvider`),
// une erreur est lancée pour prévenir l'utilisateur que le hook doit être utilisé à l'intérieur d'un `ThemeProvider`.
if (!context) {
throw new Error('useTheme doit être utilisé dans un ThemeProvider');
}
// Si le contexte est trouvé, il est renvoyé. Ce `context` contient les valeurs fournies par le `ThemeProvider`,
// comme `theme` et `basculerTheme`.
return context;
};
Dans cet exemple, ThemeProvider
enveloppe les composants de l'application. Les composants peuvent ensuite utiliser le hook useTheme
pour accéder à la valeur du thème.
import { ThemeProvider, useTheme } from "./Theme/ThemeContext"
import './Theme/theme.css';
// Composant qui utilise le thème dynamique.
const ThemedComponent: React.FC = () => {
// Utilisation du hook `useTheme` pour accéder aux valeurs du contexte.
const { theme, basculerTheme } = useTheme();
return (
<div>
<h1>Themed Component</h1>
<p>Le thème actuel est {theme}</p>
<button onClick={basculerTheme}>Basculer le thème</button>
</div>
);
};
const App: React.FC = () => {
return (
<>
<ThemeProvider>
<div>
<h1>Mon Application</h1>
<ThemedComponent />
{/* Vous pouvez ajouter ici d'autres composants comme Compteur, Lorem, etc. */}
</div>
</ThemeProvider>
</>
);
}
export default App;
/* App.css */
.theme-clair {
background-color: #ffffff;
color: #000000;
}
.theme-sombre {
background-color: #333333;
color: #ffffff;
}
button {
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
border: none;
background-color: #008cba;
color: white;
}
button:hover {
background-color: #005f6b;
}
Exercice : Authentification d'un utilisateur
Imaginons que vous ayez un état global pour l'authentification de l'utilisateur. useContext
peut être utilisé pour partager cet état entre différents composants.
Commençons par définir un type utlisateur.
export type Utilisateur = {
nom: string;
email: string;
}
import React, { createContext, useState, ReactNode, useContext } from 'react';
import { Utilisateur } from './User.ts';
// Définition du type pour le contexte (le type utilisateur peut être amélioré selon vos besoins)
type AuthContextType = {
utilisateurConnecte: Utilisateur | null;
connecterUtilisateur: (utilisateur: Utilisateur) => void;
deconnecterUtilisateur: () => void;
};
// Création du contexte avec une valeur par défaut de `undefined`
const AuthContext = createContext<AuthContextType | undefined>(undefined);
// Typage des props du `AuthProvider`
type AuthProviderProps = {
children: ReactNode; // children est de type ReactNode, ce qui permet d'accepter n'importe quel élément React
};
// `AuthProvider` gère le contexte de l'authentification
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [utilisateurConnecte, setUtilisateurConnecte] = useState<Utilisateur | null>(null);
const connecterUtilisateur = (utilisateur: Utilisateur) => {
// Ici, vous pouvez ajouter la logique de connexion via une API
// Si tout est bon, on sauvegarde l'utilisateur dans le contexte
setUtilisateurConnecte(utilisateur);
};
const deconnecterUtilisateur = () => {
setUtilisateurConnecte(null);
};
return (
<AuthContext.Provider value={{ utilisateurConnecte, connecterUtilisateur, deconnecterUtilisateur }}>
{children} {/* Affichage des enfants passés au provider */}
</AuthContext.Provider>
);
};
//hook personnalisé
export const useAuth = (): AuthContextType => {
// Récupération du contexte d'authentification
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
};
Le code précédent permet à tout composant qui sera entre deux balises <AuthContext></AuthContext>
d'accéder à l'utilisateur connecté, et aux deux méthodes permettant de le mettre à jour. On évite ainsi des successions de passages de props entre composants enfants (via leurs composant parents)
import React from 'react';
import { useAuth } from './AuthProvider'; // Utilise `useAuth` depuis son module d'origine
//Ce composant permet de connecter un utlisateur et d'afficher un méssage en fonction de son état de connection.
const UtilisateurConnecte: React.FC = () => {
// Accéder aux actions et à l'utilisateur connecté
//on fait appel au hook personnalisé
//utilisateurConnecte vaut null si personne n'est connecté
//les deux autres variables sont des accès aux deux méthodes du comopsant précédent.
const { utilisateurConnecte, connecterUtilisateur, deconnecterUtilisateur } = useAuth();
const handleConnexion = () => {
// Exemple d'utilisateur simulé
const utilisateur = {
nom: "Jean Dupont",
email: "jean.dupont@example.com",
};
// Connecter un utilisateur en faisant appel à la fonction récupérée par useAuth();
// A VOUS DE JOUER
};
const handleDeconnexion = () => {
// Connecter un utilisateur en faisant appel à la fonction récupérée par useAuth();
// A VOUS DE JOUER
};
return (
<div>
{
{/*TESTER ICI SI UN UTILISATEUR EST CONNECTE OU PAS*/} ? (
<div>
<h2>Bienvenue {/*AFFICHER SON NOM*/} !</h2>
<p>Email : {/*AFFICHER SON EMAIL*/}</p>
<button onClick={/*APPEL DE LA FONCTION POUR SE DECONNECTER*/}>Déconnecter</button>
</div>
) : (
<div>
<p>Aucun utilisateur connecté.</p>
<button onClick={/*APPEL DE LA FONCTION POUR SE DECONNECTER*/}>Connecter un utilisateur</button>
</div>
)}
</div>
);
};
export default UtilisateurConnecte;
Et on termine on intégrant <UtilisateurConnecte />
dans App.tsx
return (
<>
{/*Le composant suivant doit être à l'intérieur du composant AuthContext*/}
<UtilisateurConnecte />
</>
);