From 5611a6dc819868d875d3375e988cc6ecb7ff3dd2 Mon Sep 17 00:00:00 2001 From: GauthierWebDev Date: Fri, 11 Apr 2025 19:08:18 +0200 Subject: [PATCH] feat: Add documentation for useContext hook in React --- app/components/md/Tabs.tsx | 4 +- app/data/docs/react/usecontext/page.md | 537 ++++++++++++++++++++++ app/data/docs/react/usereducer/page.md | 599 +++++++++++++++++++++++++ 3 files changed, 1138 insertions(+), 2 deletions(-) create mode 100644 app/data/docs/react/usecontext/page.md create mode 100644 app/data/docs/react/usereducer/page.md diff --git a/app/components/md/Tabs.tsx b/app/components/md/Tabs.tsx index 07f0894..84a23f6 100644 --- a/app/components/md/Tabs.tsx +++ b/app/components/md/Tabs.tsx @@ -54,7 +54,7 @@ export function Tabs({ >
-
    +
      {tabs.map((tab) => (
    • selectTab(tab.value)} /> @@ -62,7 +62,7 @@ export function Tabs({ ))}
-
{children}
+
{children}
); diff --git a/app/data/docs/react/usecontext/page.md b/app/data/docs/react/usecontext/page.md new file mode 100644 index 0000000..7cb902a --- /dev/null +++ b/app/data/docs/react/usecontext/page.md @@ -0,0 +1,537 @@ +--- +title: Le hook useContext de React +description: Découvrez comment utiliser le hook useContext de React pour gérer les contextes dans vos applications. +tags: [Frontend, React, JavaScript, TypeScript, Bibliothèque, Interface utilisateur (UI)] +--- + +Les contextes sont un moyen de diffuser des données au travers des composants, sans avoir à les passer explicitement à chaque composant. + +Pour faire simple, imaginons une arborescence de plusieurs composants imbriqués les uns dans les autres : + +{% tabs defaultSelectedTab="jsx" %} + +{% tab value="jsx" label="JSX" %} + +```jsx +import { useState } from "react"; + +const App = () => { + const [theme, setTheme] = useState("light"); + + return ; +}; + +const A = ({ theme, setTheme }) => { + return ; +}; + +const B = ({ theme, setTheme }) => { + return ; +}; +``` + +{% /tab %} + +{% tab value="tsx" label="TSX" %} + +```tsx +import type { Dispatch, SetStateAction } from "react"; + +import { useState } from "react"; + +type Theme = "light" | "dark"; + +const App = () => { + const [theme, setTheme] = useState("light"); + + return ; +}; + +const A = ({ theme, setTheme }: { theme: Theme; setTheme: Dispatch> }) => { + return ; +}; + +const B = ({ theme, setTheme }: { theme: Theme; setTheme: Dispatch> }) => { + return ; +}; +``` + +{% /tab %} + +{% /tabs %} + +Fastidieux, n'est-ce pas ? On transmet à chaque fois les mêmes données, et ce, à chaque niveau de l'arborescence. + +C'est là que les contextes entrent en jeu ! +On va pouvoir alors déclarer notre contexte _(qui contiendra les données à diffuser)_ et le fournir à un niveau supérieur de l'arborescence. + +## Déclaration d'un contexte + +Avant de penser à notre contexte, on va réfléchir à ce que l'on veut diffuser et les valeurs par défaut. +Si on reprend notre exemple avec le thème clair et sombre, on sait que l'on va vouloir diffuser la valeur du thème et une fonction pour le changer. + +On va donc préparer le terrain en créant un fichier `ThemeContext.jsx` _(ou `ThemeContext.tsx` si tu utilises TypeScript)_ : + +{% tabs defaultSelectedTab="jsx" %} + +{% tab value="jsx" label="JSX" %} + +```jsx +import { createContext } from "react"; + +// On crée notre contexte, avec une valeur par défaut : un thème clair +const ThemeContext = createContext({ + theme: "light", + setTheme: () => {}, +}); +``` + +{% /tab %} + +{% tab value="tsx" label="TSX" %} + +```tsx +import type { Dispatch, SetStateAction } from "react"; + +import { createContext } from "react"; + +// On crée un type pour les valeurs de thème +export type Theme = "light" | "dark"; + +// On crée un type pour notre contexte +type ThemeContextType = { + theme: Theme; + setTheme: Dispatch>; +}; + +// On crée notre contexte, avec une valeur par défaut : un thème clair +const ThemeContext = createContext({ + theme: "light", + setTheme: () => {}, +}); +``` + +{% /tab %} + +{% /tabs %} + +## Fournir un contexte + +Maintenant on peut le dire : notre contexte est prêt à être utilisé ! +Il ne reste plus qu'à le fournir à notre arborescence de composants en lui créant un `Provider`. + +{% callout type="question" title="Un provider ?" %} + +Un `Provider` est un composant qui va permettre de **diffuser** les données du contexte à ses enfants. +Il est important de noter que le `Provider` doit **englober** les composants qui vont utiliser le contexte. + +{% /callout %} + +Un contexte React est un objet qui contient deux propriétés : `Provider` et `Consumer`. + +Le `Provider` est un composant qui va permettre de diffuser les données du contexte à ses enfants. +Le `Consumer` est un composant qui va permettre de récupérer les données du contexte. + +{% tabs defaultSelectedTab="jsx" %} + +{% tab value="jsx" label="JSX" %} + +```jsx +import { useState } from "react"; + +const App = () => { + const [theme, setTheme] = useState("light"); + + return ( + + + + ); +}; +``` + +{% /tab %} + +{% tab value="tsx" label="TSX" %} + +```tsx +import type { Theme } from "./ThemeContext"; + +import { useState } from "react"; + +const App = () => { + const [theme, setTheme] = useState("light"); + + return ( + + + + ); +}; +``` + +{% /tab %} + +{% /tabs %} + +Mais on peut aller encore plus loin, en créant un Provider dédié à notre contexte ! +Cela permettra de simplifier l'arborescence de composants et de rendre le code plus lisible : + +{% tabs defaultSelectedTab="jsx" %} + +{% tab value="jsx" label="JSX" %} + +```jsx +import { createContext, useState } from "react"; + +const ThemeContext = createContext({ + theme: "light", + setTheme: () => {}, +}); + +const ThemeProvider = ({ children }) => { + const [theme, setTheme] = useState("light"); + + return {children}; +}; + +export { ThemeContext, ThemeProvider }; +``` + +{% /tab %} + +{% tab value="tsx" label="TSX" %} + +```tsx +import type { ReactNode } from "react"; + +import { createContext, useState } from "react"; + +export type Theme = "light" | "dark"; + +type ThemeContextType = { + theme: Theme; + setTheme: Dispatch>; +}; + +const ThemeContext = createContext({ + theme: "light", + setTheme: () => {}, +}); + +type ThemeProviderProps = { + children: ReactNode; +}; + +const ThemeProvider = ({ children }: ThemeProviderProps) => { + const [theme, setTheme] = useState("light"); + + return {children}; +}; + +export { ThemeContext, ThemeProvider }; +``` + +{% /tab %} + +{% /tabs %} + +Et pour terminer, on va maintenant pouvoir directement imbriquer notre `ThemeProvider` dans notre `App` : + +```jsx +import { ThemeProvider } from "./ThemeContext"; + +const App = () => { + return ( + + + + ); +}; +``` + +## Utilisation d'un contexte + +C'est bien beau de créer un contexte, mais comment l'utiliser ? +Tu te souviens peut-être du `Consumer` que l'on a évoqué plus tôt, non ? + +Et bien, il est temps de le mettre en pratique ! 😁 + +Pour commencer, nous allons avoir besoin du hook `useContext` de React. +Ce hook va nous permettre de récupérer les données du contexte, et ce, directement dans nos composants. + +```jsx +import { ThemeContext } from "./ThemeContext"; +import { useContext } from "react"; + +const C = () => { + const { theme, setTheme } = useContext(ThemeContext); + + return <>{/** JSX */}; +}; +``` + +Pas mal, non ? 😉 +Fini l'arborescence de composants à rallonge, on peut maintenant récupérer les données du contexte directement dans nos composants ! + +## Les défauts des contextes + +Seulement... Un grand pouvoir implique de grandes responsabilités. 🕷️ + +Bien que les contextes soient très pratiques, il faut prendre en compte quelques points : + +- On ne peut pas utiliser les contextes pour tout et n'importe quoi. Ils sont plutôt adaptés pour diffuser des données qui sont utilisées par plusieurs composants. +- Les contextes peuvent rendre le code plus difficile à comprendre. +- L'utilisation de nombreux contextes va faire apparaître ce qu'on appelle le **context hell**. + +### Le context hell + +Dans cet article, nous avons vu comment créer un contexte et l'utiliser. +Et par chance, nous n'avons pas encore rencontré le **context hell**. + +Mais maintenant, que se passe-t-il si on a besoin de plusieurs contextes _(plusieurs dizaines par exemple !)_ dans notre application ? +On va se retrouver avec une arborescence de composants qui va devenir de plus en plus difficile à comprendre et à maintenir. + +Et c'est ça, le **context hell**. + +```jsx +root.render( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + , +); +``` + +Maintenant, demande à un développeur d'inverser le provider `UserProvider` avec le provider `NoteProvider`. +C'est jouable sans difficulté, mais si tu entends des cris de désespoir, c'est normal. 😅 + +Pour éviter de tomber dans le **context hell**, il est important de bien réfléchir à l'utilisation des contextes dans notre application avec ces quelques questions : + +- Est-ce que l'utilisation d'un contexte est vraiment nécessaire pour ce cas d'usage ? +- Est-ce que le contexte est utilisé par plusieurs composants ? +- Est-ce que le contexte est utilisé par des composants éloignés dans l'arborescence ? + +Mais alors, si tu as besoin d'autant de contextes dans ton application, comment faire ? +Et bien, il existe des solutions pour éviter le **context hell** : + +- Utiliser des bibliothèques tierces comme Redux _(solution lourde, mais très puissante)_ +- Créer un nouveau composant qui va regrouper tous les contextes _(solution plus légère, mais plus difficile à maintenir)_ + +N'étant pas un grand fan de Redux, je vais plutôt te présenter la deuxième solution. +Mais si tu veux en savoir plus sur Redux, n'hésite pas à consulter la documentation officielle ! + +### Résoudre le context hell avec un composant dédié + +Parlons de ce fameux composant qui va regrouper tous les contextes ! +On ne parle pas ici d'un simple composant Providers qui va imbriquer tous les Provider de nos contextes, mais d'une solution plus élégante. + +Après tout, nous sommes des feignants développeurs, non ? 😏 + +Réfléchissons à ce que l'on veut faire : + +- On veut pouvoir regrouper tous les contextes dans un seul composant. +- On veut pouvoir ajouter ou supprimer des contextes facilement. +- On veut pouvoir facilement les ordonner entre eux. +- On veut éviter le **context hell**. + +Et si on créait un composant Providers qui va nous permettre de faire tout ça ? + +{% tabs defaultSelectedTab="jsx" %} + +{% tab value="jsx" label="JSX" %} + +```jsx +const Providers = ({ providers, children }) => { + return ( + <> + {/** Ouverture des providers */} + {children} + {/** Fermeture des providers */} + + ); +}; +``` + +{% /tab %} + +{% tab value="tsx" label="TSX" %} + +```tsx +import type { ReactNode } from "react"; + +type ProvidersProps = { + providers: ReactNode[]; + children: ReactNode; +}; + +const Providers = ({ providers, children }: ProvidersProps) => { + return ( + <> + {/** Ouverture des providers */} + {children} + {/** Fermeture des providers */} + + ); +}; +``` + +{% /tab %} + +{% /tabs %} + +Ici on ne va pas remettre une cascade de Provider comme on a pu le voir plus tôt. +On va chercher à créer une fonction qui va nous permettre de les imbriquer les uns dans les autres. + +{% tabs defaultSelectedTab="jsx" %} + +{% tab value="jsx" label="JSX" %} + +```jsx +const nest = (children, component) => { + return React.cloneElement(component, {}, children); +}; +``` + +{% /tab %} + +{% tab value="tsx" label="TSX" %} + +```tsx +const nest = (children: ReactNode, component: ReactNode) => { + return React.cloneElement(component, {}, children); +}; +``` + +{% /tab %} + +{% /tabs %} + +{% callout type="note" title="React.cloneElement" %} + +`React.cloneElement` est une fonction qui va permettre de cloner un élément React en lui passant de nouvelles propriétés. +Cela va nous permettre de créer une nouvelle arborescence de composants sans modifier l'arborescence actuelle. + +Le premier argument est l'élément à cloner _(le composant)_, et le deuxième argument est un objet contenant les nouvelles propriétés. +Le troisième argument est le contenu de l'élément cloné _(les enfants)_. + +{% /callout %} + +Et maintenant, on va pouvoir utiliser notre fonction `nest` pour imbriquer nos Provider en utilisant la méthode `reduceRight` : + +{% tabs defaultSelectedTab="jsx" %} + +{% tab value="jsx" label="JSX" %} + +```jsx +const nest = (children, component) => { + return React.cloneElement(component, {}, children); +}; + +const Providers = ({ providers, children }) => { + return providers.reduceRight(nest, children); +}; +``` + +{% /tab %} + +{% tab value="tsx" label="TSX" %} + +```tsx +import type { ReactNode } from "react"; + +type ProvidersProps = { + providers: ReactNode[]; + children: ReactNode; +}; + +const nest = (children: ReactNode, component: ReactNode) => { + return React.cloneElement(component, {}, children); +}; + +const Providers = ({ providers, children }: ProvidersProps) => { + return providers.reduceRight(nest, children); +}; +``` + +{% /tab %} + +{% /tabs %} + +{% callout type="note" title="reduceRight" %} + +reduceRight est une méthode qui va permettre de réduire un tableau _(ou un objet)_ en appliquant une fonction de rappel de droite à gauche. +Cela va nous permettre de réduire un tableau de `Provider` en les imbriquant les uns dans les autres sans se soucier de l'ordre _(qui est défini par le tableau)_. + +Dans l'idée, on commence par le **dernier** élément du tableau, et on l'imbrique avec l'élément **précédent** du tableau et ainsi de suite jusqu'au **premier** élément du tableau. +Chaque itération va créer un nouvel élément imbriqué dans le précédent, en appelant la fonction `nest` qui est passée en argument. + +{% /callout %} + +Et voilà ! Il ne nous reste plus qu'à utiliser notre composant `Providers` pour regrouper tous nos `Provider` : + +```jsx +root.render( + + , + , + , + , + , + , + , + // ... + ]} + > + + + , +); +``` + +Évidemment le fichier contiendra toujours beaucoup de lignes, mais au moins, on a évité le **context hell** ! +Il sera nettement plus facile de modifier l'ordre des Provider ou d'en ajouter de nouveaux. + +## Conclusion + +Ça casse un peu la tête, mais les contextes sont un outil très puissant pour diffuser des données dans nos applications React. + +C'est aussi une excellente solution pour éviter d'utiliser des bibliothèques tierces comme Redux _(qui est très bien, mais qui peut être un peu lourd pour des petites applications)_. +On prendra d'ailleurs le temps de parler de Redux et de Zustand dans un prochain article 😉 + +Et si tu as besoin de plusieurs contextes dans ton application, n'oublie pas de réfléchir à l'utilisation de notre composant Providers pour éviter le **context hell**. diff --git a/app/data/docs/react/usereducer/page.md b/app/data/docs/react/usereducer/page.md new file mode 100644 index 0000000..139bcf1 --- /dev/null +++ b/app/data/docs/react/usereducer/page.md @@ -0,0 +1,599 @@ +--- +title: L'utilisation des reducers avec React +description: Découvre les hooks de React, une fonctionnalité qui te permet de gérer le state et le cycle de vie de tes composants fonctionnels. +tags: [Frontend, React, JavaScript, TypeScript, Bibliothèque, Interface utilisateur (UI)] +--- + +Si tu as lu les précédentes pages concernant les hooks de React _(useState, useEffect et useContext)_, tu as déjà une bonne vision de la manière dont tu peux concevoir une application React. + +Mais si je te dis que tu peux aller encore plus loin avec useReducer pour la gestion des états, est-ce que tu serais intéressé·e ? 🤔 + +{% callout type="question" title="Pourquoi ? useState ne suffit pas ?" %} + +Le hook `useState` est génial et essentiel pour gérer l'état local d'un composant, mais il n'est pas adapté pour des états dits "complexes" ou pour des états qui dépendent les uns des autres. + +{% /callout %} + +## Qu'est-ce que le hook useReducer ? + +Le hook `useReducer` est une alternative à `useState` qui est plus adaptée pour gérer des **états complexes** ou des **états qui dépendent les uns des autres**. + +Il est basé sur le concept de **reducers** que l'on peut retrouver dans la bibliothèque Redux. + +Un reducer est une fonction qui prend en paramètre un état et une action, et qui retourne un nouvel état. Il permet un découpage plus fin de la logique de gestion de l'état. + +Mais avant de rentrer dans les détails, donnons des exemples de quand utiliser `useReducer` ou `useState` ! + +### Quand utiliser useState ? + +Si tu dois stocker un état simple, comme un booléen, un nombre ou une chaîne de caractères, alors `useState` est parfait pour cela. + +Ne te casse donc pas la tête à remplacer tous tes `useState` par des `useReducer` si tu n'en as pas besoin. 😅 + +### Quand utiliser useReducer ? + +Dès que l'on a des états complexes ou des états qui dépendent les uns des autres, il est recommandé d'utiliser `useReducer`. + +Par exemple, si tu as un formulaire avec plusieurs champs, et que tu veux gérer l'état de chaque champ de manière indépendante, alors `useReducer` est une bonne solution. + +Ça te permettra d'éviter de créer des tonnes de `useState` et/ou handlers pour chaque champ du formulaire. + +Mais `useReducer` n'est pas seulement utile pour les formulaires, il peut être utilisé dans de nombreux cas, comme la gestion d'un panier d'achat, la gestion d'une ressource, etc. + +## À quoi ressemble un reducer ? + +Comme expliqué plus tôt, un reducer est une fonction qui prend en paramètre un état et une action, et qui retourne un nouvel état. + +Parlons dans un premier temps de la signature d'un reducer : + +{% tabs defaultSelectedTab="jsx" %} + +{% tab value="jsx" label="JSX" %} + +```jsx +const reducer = (state, action) => { + switch (action.type) { + case "TYPE_1": + return { ...state /* Nouvel état */ }; + case "TYPE_2": + return { ...state /* Nouvel état */ }; + default: + return state; + } +}; +``` + +{% /tab %} + +{% tab value="tsx" label="TSX" %} + +```tsx +const reducer = (state: State, action: Action) => { + switch (action.type) { + case "TYPE_1": + return { ...state /* Nouvel état */ }; + case "TYPE_2": + return { ...state /* Nouvel état */ }; + default: + return state; + } +}; +``` + +{% /tab %} + +{% /tabs %} + +Comme tu peux le voir, on récupère bien deux paramètres : `state` et `action`. + +Le `state` est l'état actuel du composant, et l'`action` est un objet qui contient un type et d'autres propriétés. + +En fonction du type de l'action, on retourne un nouvel état. +Par défaut _(c'est-à-dire si le type de l'action n'est pas reconnu)_, on retourne l'état actuel sans l'altérer. + +Dans le reducer, il est strictement impossible d'altérer l'état actuel directement. +L'état est contraint au principe d'**immutabilité**. + +On fera donc des `return` de l'état actuel avec les modifications nécessaires. + +{% callout type="note" title="Pourquoi déverser le contenu de l'état actuel ?" %} + +Si on ne déverse pas le contenu de l'état actuel, on perdrait les propriétés qui ne sont pas modifiées par l'action. + +En déversant le contenu de l'état actuel, on s'assure de ne pas perdre ces propriétés. + +Par exemple : + +```js +const initialState = { count: 0, message: "Hello" }; + +const reducer = (state, action) => { + switch (action.type) { + case "INCREMENT": + return { count: state.count + 1 }; + default: + return state; + } +}; +``` + +On perdrait ici la propriété `message` si on ne la déversait pas dans le nouvel état. + +{% /callout %} + +## Comment utiliser useReducer ? + +Maintenant que tu as une idée de ce qu'est un reducer, voyons comment l'utiliser avec le hook `useReducer` au sein d'une application React ! 🚀 + +Naturellement, on va commencer par importer le hook `useReducer` : + +```js +import { useReducer } from "react"; +``` + +Ensuite, on va définir notre état initial : + +{% tabs defaultSelectedTab="js" %} +{% tab value="js" label="JavaScript" %} + +```js +const initialState = { count: 0 }; +``` + +{% /tab %} + +{% tab value="ts" label="TypeScript" %} + +```ts +type State = { + count: number; +}; +const initialState: State = { count: 0 }; +``` + +{% /tab %} + +{% /tabs %} + +On peut maintenant définir notre reducer : + +{% tabs defaultSelectedTab="js" %} + +{% tab value="js" label="JavaScript" %} + +```js +const reducer = (state, action) => { + switch (action.type) { + case "INCREMENT": + return { ...state, count: state.count + 1 }; + case "DECREMENT": + return { ...state, count: state.count - 1 }; + case "RESET": + return { ...state, count: 0 }; + case "SET": + return { ...state, count: action.payload }; + default: + return state; + } +}; +``` + +{% /tab %} + +{% tab value="ts" label="TypeScript" %} + +```ts +type State = { + count: number; +}; + +type Action = { + type: "INCREMENT" | "DECREMENT" | "RESET" | "SET"; + payload?: number; +}; + +const reducer = (state: State, action: Action) => { + switch (action.type) { + case "INCREMENT": + return { ...state, count: state.count + 1 }; + case "DECREMENT": + return { ...state, count: state.count - 1 }; + case "RESET": + return { ...state, count: 0 }; + case "SET": + return { ...state, count: action.payload! }; + default: + return state; + } +}; +``` + +{% /tab %} + +{% /tabs %} + +{% callout type="question" title="C'est quoi `action.payload` ?" %} + +La propriété `payload` de l'action est optionnelle. Il s'agit d'une convention pour passer des données à l'action. + +Le `!` après `action.payload` signifie que l'on est sûr que `payload` est défini. +Cela permet d'éviter une erreur de type avec TypeScript. + +Dans le cas du type `SET`, le payload sera défini obligatoirement avec un nombre qui sera la nouvelle valeur de la propriété `count` de l'état. + +{% /callout %} + +Enfin, on peut utiliser le hook useReducer dans notre composant : + +{% tabs defaultSelectedTab="js" %} + +{% tab value="js" label="JavaScript" %} + +```js +const [state, dispatch] = useReducer(reducer, initialState); +``` + +{% /tab %} + +{% tab value="ts" label="TypeScript" %} + +```ts +const [state, dispatch] = useReducer(reducer, initialState); +``` + +{% /tab %} + +{% /tabs %} + +`state` contient l'état actuel, et `dispatch` est une fonction qui permet d'envoyer une action au reducer. + +Pour modifier l'état, on va donc appeler `dispatch` avec une action : + +```js +dispatch({ type: "INCREMENT" }); +``` + +Et voilà, tu sais maintenant comment utiliser `useReducer` dans une application React ! 🎉 + +## On nettoie tout ça ! + +Tout ce qui t'a été montré plus haut fonctionne, mais est-ce que pour autant ce code est qualitatif ? 🤔 + +**Non !** + +Pourquoi ? Déjà, on a tout mis dans le même fichier, ce qui n'est pas très propre. +Mais surtout, notre code n'est pas à l'abri d'erreurs. + +Que se passerait-il si on se trompait dans le type de l'action ? +Ou si on oubliait de passer un payload à l'action `SET` ? + +C'est ce genre de comportements que l'on veut éviter pour nous assurer le bon fonctionnement de notre application. + +Pour contrer ces problèmes, on va créer des actions et des types d'actions pour garantir la cohérence de notre code. + +### Création des types d'actions + +Nos types d'actions seront tous des chaînes de caractères. On va donc pouvoir les définir sous forme de constantes. + +```js +export const INCREMENT = "INCREMENT"; +export const DECREMENT = "DECREMENT"; +export const RESET = "RESET"; +export const SET = "SET"; +``` + +{% callout type="note" title="Regrouper les exports" %} + +Et là, tu te dis : "Pourquoi ne pas regrouper les exports dans un seul objet ?" + +Bien vu ! Et pour TypeScript, on peut aller encore plus loin en créant un `enum` pour les types d'actions 😉 + +{% tabs defaultSelectedTab="js" %} + +{% tab value="js" label="JavaScript" %} + +```js +export const CounterActionTypes = { + INCREMENT: "INCREMENT", + DECREMENT: "DECREMENT", + RESET: "RESET", + SET: "SET", +}; +``` + +{% /tab %} + +{% tab value="ts" label="TypeScript" %} + +```ts +export const enum CounterActionTypes { + INCREMENT = "INCREMENT", + DECREMENT = "DECREMENT", + RESET = "RESET", + SET = "SET", +} +``` + +{% /tab %} + +{% /tabs %} + +{% /callout %} + +### Typage des actions + +Si tu utilises JavaScript, je suis désolé de te dire que tu ne peux pas **fortement** typer les actions. + +En revanche, si tu utilises TypeScript, tu peux définir les actions de la manière suivante : + +```ts +export type CounterAction = + | { type: CounterActionTypes.INCREMENT } + | { type: CounterActionTypes.DECREMENT } + | { type: CounterActionTypes.RESET } + | { type: CounterActionTypes.SET; payload: number }; +``` + +Tu pourras alors utiliser `CounterAction` pour typer les actions de ton reducer : + +```ts +const reducer = (state: State, action: CounterAction) => { + switch (action.type) { + case CounterActionTypes.INCREMENT: + return { ...state, count: state.count + 1 }; + case CounterActionTypes.DECREMENT: + return { ...state, count: state.count - 1 }; + case CounterActionTypes.RESET: + return { ...state, count: 0 }; + case CounterActionTypes.SET: + return { ...state, count: action.payload }; + default: + return state; + } +}; +``` + +### Action creators + +Pour éviter de se tromper dans le type de l'action, on peut se créer des fonctions qui vont nous permettre de créer des actions. + +{% tabs defaultSelectedTab="js" %} + +{% tab value="js" label="JavaScript" %} + +```js +export const actions = { + increment: () => ({ type: CounterActionTypes.INCREMENT }), + decrement: () => ({ type: CounterActionTypes.DECREMENT }), + reset: () => ({ type: CounterActionTypes.RESET }), + set: (value) => ({ type: CounterActionTypes.SET, payload: value }), +}; +``` + +{% /tab %} + +{% tab value="ts" label="TypeScript" %} + +```ts +export const actions = { + increment: (): CounterAction => ({ type: CounterActionTypes.INCREMENT }), + decrement: (): CounterAction => ({ type: CounterActionTypes.DECREMENT }), + reset: (): CounterAction => ({ type: CounterActionTypes.RESET }), + set: (value: number): CounterAction => ({ type: CounterActionTypes.SET, payload: value }), +}; +``` + +{% /tab %} + +{% /tabs %} + +Maintenant le dispatch de nos actions sera beaucoup plus simple et éviter davantage les erreurs lors du développement ! + +```js +dispatch(actions.increment()); +dispatch(actions.set(10)); +``` + +## Les fichiers complets + +On a vu beaucoup de chose et les fichiers sont un peu éparpillés. +Pour t'aider à mieux comprendre le fonctionnement du hook `useReducer` et comment l'implementer, voici les fichiers complets : + +### Fichier `counterReducer.js` ou `counterReducer.ts` + +{% tabs defaultSelectedTab="js" %} + +{% tab value="js" label="JavaScript" %} + +```js +const CounterActionTypes = { + INCREMENT: "INCREMENT", + DECREMENT: "DECREMENT", + RESET: "RESET", + SET: "SET", +}; + +export const initialState = { count: 0 }; + +export const reducer = (state, action) => { + switch (action.type) { + case CounterActionTypes.INCREMENT: + return { ...state, count: state.count + 1 }; + case CounterActionTypes.DECREMENT: + return { ...state, count: state.count - 1 }; + case CounterActionTypes.RESET: + return { ...state, count: 0 }; + case CounterActionTypes.SET: + return { ...state, count: action.payload }; + default: + return state; + } +}; + +export const actions = { + increment: () => ({ type: CounterActionTypes.INCREMENT }), + decrement: () => ({ type: CounterActionTypes.DECREMENT }), + reset: () => ({ type: CounterActionTypes.RESET }), + set: (value) => ({ type: CounterActionTypes.SET, payload: value }), +}; +``` + +{% /tab %} + +{% tab value="ts" label="TypeScript" %} + +```ts +const enum CounterActionTypes { + INCREMENT = "INCREMENT", + DECREMENT = "DECREMENT", + RESET = "RESET", + SET = "SET", +} + +type State = { + count: number; +}; + +type Action = + | { type: CounterActionTypes.INCREMENT } + | { type: CounterActionTypes.DECREMENT } + | { type: CounterActionTypes.RESET } + | { type: CounterActionTypes.SET; payload: number }; + +export const initialState: State = { count: 0 }; + +export const reducer = (state: State, action: Action) => { + switch (action.type) { + case CounterActionTypes.INCREMENT: + return { ...state, count: state.count + 1 }; + case CounterActionTypes.DECREMENT: + return { ...state, count: state.count - 1 }; + case CounterActionTypes.RESET: + return { ...state, count: 0 }; + case CounterActionTypes.SET: + return { ...state, count: action.payload }; + default: + return state; + } +}; + +export const actions = { + increment: (): Action => ({ type: CounterActionTypes.INCREMENT }), + decrement: (): Action => ({ type: CounterActionTypes.DECREMENT }), + reset: (): Action => ({ type: CounterActionTypes.RESET }), + set: (value: number): Action => ({ type: CounterActionTypes.SET, payload: value }), +}; +``` + +{% /tab %} + +{% /tabs %} + +### Fichier `Counter.jsx` ou `Counter.tsx` + +{% tabs defaultSelectedTab="jsx" %} + +{% tab value="jsx" label="JSX" %} + +```jsx +import { initialState, actions, reducer } from "../reducers/counterReducer"; +import { useReducer } from "react"; + +const Counter = () => { + const [state, dispatch] = useReducer(reducer, initialState); + + return ( +
+

Count: {state.count}

+ + + + + + + + +
+ ); +}; + +export default Counter; +``` + +{% /tab %} + +{% tab value="tsx" label="TSX" %} + +```tsx +import { initialState, actions, reducer } from "../reducers/counterReducer"; +import { useReducer } from "react"; + +const Counter = () => { + const [state, dispatch] = useReducer(reducer, initialState); + + return ( +
+

Count: {state.count}

+ + + + + + + + +
+ ); +}; + +export default Counter; +``` + +{% /tab %} + +{% /tabs %} + +## C'est l'heure des questions ! + +{% callout type="question" title="Quand utiliser `useReducer` ?" %} + +- **A** - Pour des états simples +- **B** - Pour des états complexes ou des états qui dépendent les uns des autres + +{% /callout %} + +{% callout type="question" title="Quelle est la signature d'un reducer ?" %} + +- **A** - `(state, action) => { /* ... */ }` +- **B** - `(action, state) => { /* ... */ }` +- **C** - `(state) => { /* ... */ }` +- **D** - `(action) => { /* ... */ }` + +{% /callout %} + +{% callout type="question" title="Pourquoi déverser le contenu de l'état actuel dans le nouvel état ?" %} + +- **A** - Pour rendre le code plus lisible +- **B** - Pour ne pas perdre les propriétés qui ne sont pas modifiées par l'action + +{% /callout %} + +{% callout type="question" title="Pourquoi utiliser des constantes pour les types d'actions ?" %} + +- **A** - Pour rendre le code plus lisible +- **B** - Pour alourdir inutillement le code +- **C** - Pour éviter de se tromper dans le type de l'action + +{% /callout %} + +## Conclusion + +Alors, pas trop fatigué·e ? 😅 +Au moins ça en valait la peine ! Tu te feras moins de nœuds au cerveau par la suite dans tes projets React ! + +Comme tu as pu le voir, `useReducer` est un outil puissant pour gérer des états complexes ou des états qui dépendent les uns des autres. + +Même si ici notre exemple n'était qu'un simple compteur, tu peux appliquer tout ce que tu as pu voir dans des cas plus concrets. + +Si tu veux aller encore plus loin, n'hésite pas à jeter un œil à la documentation officielle de React pour `useReducer`. +Tu peux également te renseigner sur Redux si tu veux aller encore plus loin dans la gestion de l'état de ton application, mais attention, c'est une autre paire de manches ! 😄