Ampliare con Reducer e Context
I Reducers ti permettono di consolidare la logica di modifica dello stato di un componente. Il Context ti permette di passare informazioni in profondità ad altri componenti. Puoi combinare i reducers e i context insieme per gestire lo stato di una schermata complessa.
Imparerai
- Come combinare un reducer con il context
- Come evitare di passare lo stato e dispatch attraverso i parametri
- Come mantenere context e logica dello stato in un file separato
Combinare un reducer con un context
In questo esempio da introduzione ai reducers, lo stato è gestito da un reducer. La funzione del reducer contiene tutta la logica di modifica dello stato ed é dichiarata in fondo al file:
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>Giorno di ferie a 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('Azione sconosciuta: ' + action.type); } } } let nextId = 3; const initialTasks = [ { id: 0, text: 'Il sentiero del filosofo', done: true }, { id: 1, text: 'Visita al tempio', done: false }, { id: 2, text: 'Bevi matcha', done: false } ];
Un reducer aiuta a mantenere i gestori degli eventi brevi e concisi. Tuttavia, man mano che la tua app cresce, potresti incontrare un’altro tipo di difficoltà. Attualmente, lo stato tasks
e la funzione dispatch
sono disponibili solo nel componente di primo livello TaskApp
. Per far in modo che gli altri componenti leggano la lista di tasks o la cambino, devi esplicitamente passare verso il basso lo stato attuale e i gestori degli eventi che la cambiano come parametri.
Per esempio, TaskApp
passa la lista di tasks e i gestori degli eventi a TaskList
:
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
E TaskList
passa i gestori degli eventi a Task
:
<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/>
In un esempio breve come questo, questo metodo funziona bene, ma se hai decine o centinaia di componenti nel mezzo, passare tutto lo stato e tutte le funzioni può essere abbastanza frustrante!
Ed ecco perchè, come alternativa a passarli tramite parametri, vorresti poter mettere sia lo stato tasks
sia la funzione dispatch
nel contesto. In questo modo, ogni component al di sotto di TaskApp
nell’albero può leggere le tasks e le azioni dispatch senza la ripetitiva “perforazione tramite parametri”.
Ecco come combinare un reducer con il context:
- Crea il context.
- Inserisci lo stato e il dispatch nel context.
- Usa il context ovunque nell’albero.
Passo 1: Crea il context
Il gancio useReducer
ritorna le tasks
attuali e la funzione dispatch
che ti permette di aggiornarle:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
Per passarli in basso nell’albero, creerai due contesti separati:
- Il
TasksContext
fornisce la lista corrente di tasks. - Il
TasksDispatchContext
fornisce la funzione che permette ai componenti l’invio di azioni.
Esportali in un file separato in modo da poter più tardi importarli in altri files:
import { createContext } from 'react'; export const TasksContext = createContext(null); export const TasksDispatchContext = createContext(null);
Qui, stai passando null
come il valore di default a tutti e due i context. I veri valori verranno forniti dal componente TaskApp
.
Passo 2: Inserisci lo stato e il dispatch nel context
Ora puoi importare entrambi i context nel tuo componente TaskApp
. Prendi tasks
e dispatch
ritornati da useReducer()
e forniscili all’intero albero sottostante:
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>
);
}
Per ora, passa le informazioni sia tramite parametri che nel context:
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>Giorno di ferie a 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('Azione sconosciuta: ' + action.type); } } } let nextId = 3; const initialTasks = [ { id: 0, text: 'Il sentiero del filosofo', done: true }, { id: 1, text: 'Visita al tempio', done: false }, { id: 2, text: 'Bevi matcha', done: false } ];
Nel prossimo passo, rimuoverai il passaggio dei parametri.
Passo 3: Usa il context ovunque nell’albero
Ora non hai bisogno di passare la lista di tasks o i gestori di eventi nell’albero sottostante:
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
<h1>Giorno di ferie a Kyoto</h1>
<AddTask />
<TaskList />
</TasksDispatchContext.Provider>
</TasksContext.Provider>
Al suo posto, ogni componente che ha bisogno della lista di task può leggerla dal TaskContext
:
export default function TaskList() {
const tasks = useContext(TasksContext);
// ...
Per aggiornare la lista di task, ogni componente può leggere la funzione dispatch
dal context e chiamarla:
export default function AddTask() {
const [text, setText] = useState('');
const dispatch = useContext(TasksDispatchContext);
// ...
return (
// ...
<button onClick={() => {
setText('');
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}}>Aggiungi</button>
// ...
Il componente TaskApp
non passa nessun gestore di eventi al di sotto, e neanche il TaskList
non passa nessun gestore di eventi al Task
. Ogni componente legge dal context ciò di cui ha bisogno:
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)}> Salva </button> </> ); } else { taskContent = ( <> {task.text} <button onClick={() => setIsEditing(true)}> Modifica </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 }); }}> Rimuovi </button> </label> ); }
Lo stato ancora “vive” nel componente di primo livello TaskApp
, gestito con useReducer
. Ma i suoi tasks
e dispatch
sono ora disponibili ad ogni componente nell’albero sottostante semplicemente importando e usando questi context.
Spostare tutte le connessioni in un unico documento
Non devi necessariamente farlo, ma potresti dividere ulteriormente i componenti muovendo sia il reducer che il context in un unico file. Attualmente, TasksContext.js
contiene solo due dichiarazioni:
import { createContext } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
Questo file sta per diventare affollato! Muovi il reducer nello stesso documento. Dopodichè dichiari il nuovo componente TasksProvider
nello stesso file. Questo componente legherà tutti i pezzi insieme:
- Gestisce lo stato con un reducer.
- Fornisce entrambi i context ai componenti sottostanti.
- Prende
children
come parametro in modo da potergli passare 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>
);
}
Questo rimuove tutta la complessità e connessioni dal tuo componente TaskApp
:
import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; import { TasksProvider } from './TasksContext.js'; export default function TaskApp() { return ( <TasksProvider> <h1>Giorno di ferie a Kyoto</h1> <AddTask /> <TaskList /> </TasksProvider> ); }
Puoi anche esportare funzioni che usano il context da TasksContext.js
:
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
Quando un componente ha bisogno di leggere il context, puoi farlo tramite queste funzioni:
const tasks = useTasks();
const dispatch = useTasksDispatch();
Questo non cambia il comportamento in alcun modo, ma ti permette di dividere più tardi ulteriormente questi context o di aggiungere della logica alle funzioni. Ora tutti i context e le connessioni dei reducer sono in TasksContext.js
. Queto mantiene i componenti puliti e ordinati, concentrandosi su cosa mostrare piuttosto che dove prendere i dati:
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)}> Salva </button> </> ); } else { taskContent = ( <> {task.text} <button onClick={() => setIsEditing(true)}> Modifica </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 }); }}> Rimuovi </button> </label> ); }
Puoi pensare a TasksProvider
come una parte di una schermata che conosce come gestire le tasks, useTasks
come un modo per leggerle e useTasksDispatch
come al modo per aggiornarle da ogni componente ad un livello inferiore nell’albero.
Man mano che la tua applicazione cresce, potresti avere tante coppie context-reducer come questa. Questo è un modo potente per far crescere la tua app e portare lo stato in alto senza troppa fatica ogni volta che vuoi accedere ai dati in fondo all’albero.
Riepilogo
- Puoi combinare reducer con il context per permettere ad ogni componente di leggere e modificare lo stato sopra di esso.
- Per fornire lo stato e la funzione dispatch ai componenti sottostanti:
- Crea due context (per stato e per la funzione dispatch).
- Fornisci entrambi i context dal componente che usa il reducer.
- Usa uno dei due context dai componente che ne hanno bisogno per leggerli.
- Puoi ulteriormente ordinare i componenti muovendo tutte le connessioni in un unico file.
- Puoi esportare un componente come
TasksProvider
che fornisce il context. - Puoi anche esportare dei Ganci personalizzati come
useTasks
euseTasksDispatch
per leggerlo.
- Puoi esportare un componente come
- Puoi avere molte coppie context-reducer come questa nella tua app.