Mise à l'échelle en combinant réducteur et contexte
Les réducteurs vous permettent de consolider la logique de mise à jour de l’état d’un composant. Le contexte vous offre la possibilité de transmettre de l’information en profondeur à d’autres composants. Vous pouvez les combiner afin de gérer l’état d’un écran complexe.
Vous allez apprendre
- Comment combiner un réducteur avec un contexte
- Comment éviter de passer l’état et le dispatcher au travers des props
- Comment conserver le contexte et la logique d’état dans un fichier séparé
Combiner un réducteur avec un contexte
Dans cet exemple provenant de l’introduction aux réducteurs, l’état est géré par un réducteur. Cette fonction (déclarée à la fin du fichier) contient toute la logique de mise à jour de l’état :
import { useReducer } from 'react'; import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; export default function TaskApp() { const [tasks, dispatch] = useReducer( tasksReducer, initialTasks ); function handleAddTask(text) { dispatch({ type: 'added', id: nextId++, text: text, }); } function handleChangeTask(task) { dispatch({ type: 'changed', task: task }); } function handleDeleteTask(taskId) { dispatch({ type: 'deleted', id: taskId }); } return ( <> <h1>Jour de repos à Kyoto</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </> ); } function tasksReducer(tasks, action) { switch (action.type) { case 'added': { return [...tasks, { id: action.id, text: action.text, done: false }]; } case 'changed': { return tasks.map(t => { if (t.id === action.task.id) { return action.task; } else { return t; } }); } case 'deleted': { return tasks.filter(t => t.id !== action.id); } default: { throw Error('Unknown action: ' + action.type); } } } let nextId = 3; const initialTasks = [ { id: 0, text: 'La promenade du philosophe', done: true }, { id: 1, text: 'Visiter le temple', done: false }, { id: 2, text: 'Boire du thé matcha', done: false } ];
Un réducteur permet de garder les gestionnaires d’événements concis. Vous pouvez cependant rencontrer une autre difficulté à mesure que votre appli grandit. Pour le moment, l’état tasks
et le fonction dispatch
ne sont disponibles qu’au niveau du composant racine TaskApp
. Pour que les autres composants puissent lire ou modifier la liste de tâches, vous devez explicitement transmettre via les props l’état courant et les gestionnaires d’événements qui le modifient.
Par exemple, TaskApp
passe une liste de tâches et les gestionnaires d’événements à TaskList
:
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
Et TaskList
passe les gestionnaires d’événements à Task
:
<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/>
Ça fonctionne bien pour un petit exemple comme celui-là, mais si vous avez des dizaines voire des centaines de composants au milieu, transmettre les états et gestionnaires d’événements peut s’avérer plutôt frustrant !
C’est pourquoi, plutôt que de les passer via les props, vous pourriez mettre l’état tasks
et la fonction dispatch
dans un contexte. De cette façon, tous les composants en dessous de TaskApp
dans l’arbre peuvent lire les tâches et dispatcher les actions sans la « percolation des props ».
Voici comment vous pouvez combiner un réducteur avec un contexte :
- Créez le contexte.
- Mettez l’état et la fonction de dispatch dans le contexte.
- Utilisez le contexte n’importe où dans l’arbre.
Étape 1 : créer le contexte
Le Hook useReducer
renvoie les tasks
courantes et la fonction dispatch
qui vous permet de les mettre à jour :
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
Pour les passer à travers l’arbre, vous aller créer deux contextes distincts :
TasksContext
fournit la liste actuelle des tâches.TasksDispatchContext
fournit la fonction qui permet aux composants de dispatcher les actions.
Exportez-les à partir d’un fichier dédié de façon à pouvoir les importer ultérieurement dans d’autres fichiers :
import { createContext } from 'react'; export const TasksContext = createContext(null); export const TasksDispatchContext = createContext(null);
Ici, vous passez null
comme valeur par défaut aux deux contextes. Les valeurs réelles seront fournies par le composant TaskApp
.
Étape 2 : placer l’état et le dispatch dans le contexte
Vous pouvez maintenant importer les deux contextes dans votre composant TaskApp
. Prenez les tasks
et dispatch
renvoyées par le useReducer()
et fournissez-les à l’arbre entier en dessous :
import { TasksContext, TasksDispatchContext } from './TasksContext.js';
export default function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
// ...
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
...
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
Pour l’instant, vous passez l’information à la fois par les props et dans le contexte :
import { useReducer } from 'react'; import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; import { TasksContext, TasksDispatchContext } from './TasksContext.js'; export default function TaskApp() { const [tasks, dispatch] = useReducer( tasksReducer, initialTasks ); function handleAddTask(text) { dispatch({ type: 'added', id: nextId++, text: text, }); } function handleChangeTask(task) { dispatch({ type: 'changed', task: task }); } function handleDeleteTask(taskId) { dispatch({ type: 'deleted', id: taskId }); } return ( <TasksContext.Provider value={tasks}> <TasksDispatchContext.Provider value={dispatch}> <h1>Jour de repos à Kyoto</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </TasksDispatchContext.Provider> </TasksContext.Provider> ); } function tasksReducer(tasks, action) { switch (action.type) { case 'added': { return [...tasks, { id: action.id, text: action.text, done: false }]; } case 'changed': { return tasks.map(t => { if (t.id === action.task.id) { return action.task; } else { return t; } }); } case 'deleted': { return tasks.filter(t => t.id !== action.id); } default: { throw Error('Unknown action: ' + action.type); } } } let nextId = 3; const initialTasks = [ { id: 0, text: 'La promenade du philosophe', done: true }, { id: 1, text: 'Visiter le temple', done: false }, { id: 2, text: 'Boire du thé matcha', done: false } ];
Vous cesserez d’utiliser les props dans la prochaine étape.
Étape 3 : utiliser un contexte n’importe où dans l’arbre
Vous n’avez désormais plus besoin de passer la liste de tâches ou les gestionnaires d’événements à travers l’arbre :
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
<h1>Jour de repos à Kyoto</h1>
<AddTask />
<TaskList />
</TasksDispatchContext.Provider>
</TasksContext.Provider>
Au lieu de ça, un composant qui a besoin de la liste de tâches peut la lire depuis le TaskContext
:
export default function TaskList() {
const tasks = useContext(TasksContext);
// ...
Pour mettre à jour la liste de tâches, un composant peut lire la fonction dispatch
depuis le contexte et l’appeler :
export default function AddTask() {
const [text, setText] = useState('');
const dispatch = useContext(TasksDispatchContext);
// ...
return (
// ...
<button onClick={() => {
setText('');
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}}>Ajouter</button>
// ...
Le composant TaskApp
ne transmet aucun gestionnaire d’événement vers le bas et la TaskList
ne transmet pas non plus de gestionnaire d’événement au composant Task
. Chaque composant lit le contexte dont il a besoin :
import { useState, useContext } from 'react'; import { TasksContext, TasksDispatchContext } from './TasksContext.js'; export default function TaskList() { const tasks = useContext(TasksContext); return ( <ul> {tasks.map(task => ( <li key={task.id}> <Task task={task} /> </li> ))} </ul> ); } function Task({ task }) { const [isEditing, setIsEditing] = useState(false); const dispatch = useContext(TasksDispatchContext); let taskContent; if (isEditing) { taskContent = ( <> <input value={task.text} onChange={e => { dispatch({ type: 'changed', task: { ...task, text: e.target.value } }); }} /> <button onClick={() => setIsEditing(false)}> Enregistrer </button> </> ); } else { taskContent = ( <> {task.text} <button onClick={() => setIsEditing(true)}> Modifier </button> </> ); } return ( <label> <input type="checkbox" checked={task.done} onChange={e => { dispatch({ type: 'changed', task: { ...task, done: e.target.checked } }); }} /> {taskContent} <button onClick={() => { dispatch({ type: 'deleted', id: task.id }); }}> Supprimer </button> </label> ); }
L’état « vit » toujours dans le composant de haut-niveau TaskApp
, où il reste géré avec useReducer
. Toutefois, ses tasks
et dispatch
sont désormais accessibles à chaque composant plus bas dans l’arbre, en important et utilisant ces contextes.
Déplacer toute cette plomberie dans un seul fichier
Vous n’êtes pas obligé·e de le faire, mais vous pouvez encore alléger les composants en déplaçant le réducteur et le contexte dans un seul fichier. Pour le moment, TasksContext.js
ne contient que les deux déclarations de contexte :
import { createContext } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
Ce fichier va très vite grandir ! Vous allez déplacer le réducteur dans ce fichier. Ensuite, vous allez y déclarer un nouveau composant TasksProvider
. Ce composant va relier toutes les pièces du puzzle :
- Il gérera l’état avec un réducteur.
- Il fournira les deux contextes aux composants en dessous.
- Il prendra les
children
en tant que props afin que vous puissiez lui donner du JSX.
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
Ça supprime toute la complexité et la plomberie de votre composant TaskApp
:
import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; import { TasksProvider } from './TasksContext.js'; export default function TaskApp() { return ( <TasksProvider> <h1>Jour de repos à Kyoto</h1> <AddTask /> <TaskList /> </TasksProvider> ); }
Vous pouvez également exporter des fonctions qui utilisent le contexte de TasksContext.js
:
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
Quand un composant a besoin de lire le contexte, il peut le faire via ces fonctions :
const tasks = useTasks();
const dispatch = useTasksDispatch();
Ça ne change en rien le comportement, mais ça vous laisse l’opportunité de diviser davantage ces contextes ou d’ajouter de la logique à ces fonctions. Toute la plomberie du contexte et du réducteur se trouvent désormais dans TasksContext.js
. Ça laisse les composants propres et épurés, et ils peuvent ainsi se concentrer sur ce qu’ils doivent afficher plutôt que savoir où chercher les données :
import { useState } from 'react'; import { useTasks, useTasksDispatch } from './TasksContext.js'; export default function TaskList() { const tasks = useTasks(); return ( <ul> {tasks.map(task => ( <li key={task.id}> <Task task={task} /> </li> ))} </ul> ); } function Task({ task }) { const [isEditing, setIsEditing] = useState(false); const dispatch = useTasksDispatch(); let taskContent; if (isEditing) { taskContent = ( <> <input value={task.text} onChange={e => { dispatch({ type: 'changed', task: { ...task, text: e.target.value } }); }} /> <button onClick={() => setIsEditing(false)}> Enregistrer </button> </> ); } else { taskContent = ( <> {task.text} <button onClick={() => setIsEditing(true)}> Modifier </button> </> ); } return ( <label> <input type="checkbox" checked={task.done} onChange={e => { dispatch({ type: 'changed', task: { ...task, done: e.target.checked } }); }} /> {taskContent} <button onClick={() => { dispatch({ type: 'deleted', id: task.id }); }}> Supprimer </button> </label> ); }
Vous pouvez voir le TasksProvider
comme une partie de l’écran qui sait comment traiter les tâches, useTasks
comme une façon de les lire et useTasksDispatch
comme un moyen de les mettre à jour à partir de n’importe quel composant plus bas dans l’arbre.
Au fur et à mesure que votre appli grandit, il se peut que vous ayez de nombreuses paires contexte-réducteur comme celle-ci. C’est un moyen puissant de faire grandir votre appli et de faire remonter l’état sans trop d’efforts chaque fois que vous avez besoin d’accéder aux données au plus profond de l’arbre.
En résumé
- Vous pouvez combiner le réducteur avec le contexte pour permettre à n’importe quel composant de lire et mettre à jour l’état qui se trouve au-dessus de lui.
- Pour fournir l’état et la fonction de dispatch aux composants en dessous :
- Créez deux contextes (l’un pour l’état et l’autre pour les fonctions de dispatch).
- Fournissez ces deux contextes à partir du composant qui utilise le réducteur.
- Utilisez l’un ou l’autre des contextes pour les composants qui ont besoin de les lire.
- Vous avez la possibilité d’alléger les composants en déplaçant toute la plomberie dans un seul fichier.
- Vous pouvez exporter un composant tel que
TasksProvider
qui fournit le contexte. - Vous pouvez également exporter des Hooks personnalisés comme
useTasks
etuseTasksDispatch
pour les lire.
- Vous pouvez exporter un composant tel que
- Vous pouvez disposer de nombreuses paires contexte-réducteur comme ça dans votre appli.