Escalando com Reducer e Contexto
Reducers permitem consolidar a lógica de atualização de estado de um componente. O contexto permite passar informações profundamente para outros componentes. Você pode combinar reducers e contexto para gerenciar o estado de uma tela complexa.
Você aprenderá
- Como combinar um reducer com contexto
- Como evitar passar estado e dispatch através de props
- Como manter a lógica de contexto e estado em um arquivo separado
Combinando um reducer com contexto
Neste exemplo da introdução aos reducers, o estado é gerenciado por um reducer. A função reducer contém toda a lógica de atualização de estado e é declarada na parte inferior deste arquivo:
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>Dia de folga em 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('Ação desconhecida: ' + action.type); } } } let nextId = 3; const initialTasks = [ { id: 0, text: 'Caminho do Filósofo', done: true }, { id: 1, text: 'Visitar o templo', done: false }, { id: 2, text: 'Beber matcha', done: false } ];
Um reducer ajuda a manter os manipuladores de eventos curtos e concisos. No entanto, à medida que seu aplicativo cresce, você pode encontrar outra dificuldade. Atualmente, o estado tasks
e a função dispatch
estão disponíveis apenas no componente de nível superior TaskApp
. Para permitir que outros componentes leiam a lista de tarefas ou a alterem, você deve explicitamente passar o estado atual e os manipuladores de eventos que o alteram como props.
Por exemplo, TaskApp
passa uma lista de tarefas e os manipuladores de eventos para TaskList
:
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
E TaskList
passa os manipuladores de eventos para Task
:
<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/>
Em um pequeno exemplo como este, isso funciona bem, mas se você tiver dezenas ou centenas de componentes no meio, passar todos os estados e funções pode ser bastante frustrante!
É por isso que, como alternativa a passar através de props, você pode querer colocar tanto o estado tasks
quanto a função dispatch
no contexto. Dessa forma, qualquer componente abaixo de TaskApp
na árvore pode ler as tarefas e despachar ações sem a repetitiva “prop drilling”.
Aqui está como você pode combinar um reducer com contexto:
- Criar o contexto.
- Colocar estado e dispatch no contexto.
- Usar o contexto em qualquer lugar na árvore.
Passo 1: Criar o contexto
O Hook useReducer
retorna os atuais tasks
e a função dispatch
que permite que você os atualize:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
Para passá-los para baixo na árvore, você irá criar dois contextos separados:
TasksContext
fornece a lista atual de tarefas.TasksDispatchContext
fornece a função que permite que componentes despachem ações.
Exporte-os de um arquivo separado para que você possa importá-los mais tarde a partir de outros arquivos:
import { createContext } from 'react'; export const TasksContext = createContext(null); export const TasksDispatchContext = createContext(null);
Aqui, você está passando null
como o valor padrão para ambos os contextos. Os valores reais serão fornecidos pelo componente TaskApp
.
Passo 2: Colocar estado e dispatch no contexto
Agora você pode importar ambos os contextos no seu componente TaskApp
. Pegue os tasks
e dispatch
retornados por useReducer()
e forneça-os para toda a árvore abaixo:
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>
);
}
Por enquanto, você passa as informações tanto via props quanto no contexto:
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>Dia de folga em 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('Ação desconhecida: ' + action.type); } } } let nextId = 3; const initialTasks = [ { id: 0, text: 'Caminho do Filósofo', done: true }, { id: 1, text: 'Visitar o templo', done: false }, { id: 2, text: 'Beber matcha', done: false } ];
No próximo passo, você irá remover a passagem de props.
Passo 3: Usar contexto em qualquer lugar na árvore
Agora você não precisa passar a lista de tarefas ou os manipuladores de eventos para baixo na árvore:
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
<h1>Dia de folga em Kyoto</h1>
<AddTask />
<TaskList />
</TasksDispatchContext.Provider>
</TasksContext.Provider>
Em vez disso, qualquer componente que precise da lista de tarefas pode lê-la do TasksContext
:
export default function TaskList() {
const tasks = useContext(TasksContext);
// ...
Para atualizar a lista de tarefas, qualquer componente pode ler a função dispatch
do contexto e chamá-la:
export default function AddTask() {
const [text, setText] = useState('');
const dispatch = useContext(TasksDispatchContext);
// ...
return (
// ...
<button onClick={() => {
setText('');
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}}>Adicionar</button>
// ...
O componente TaskApp
não passa nenhum manipulador de eventos para baixo, e o TaskList
também não passa nenhum manipulador de eventos para o componente Task
. Cada componente lê o contexto de que precisa:
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)}> Salvar </button> </> ); } else { taskContent = ( <> {task.text} <button onClick={() => setIsEditing(true)}> Editar </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 }); }}> Deletar </button> </label> ); }
O estado ainda “vive” no componente de nível superior TaskApp
, gerenciado com useReducer
. Mas seus tasks
e dispatch
agora estão disponíveis para todos os componentes abaixo na árvore, importando e usando esses contextos.
Movendo toda a ligação para um único arquivo
Você não precisa fazer isso, mas pode ainda mais limpar os componentes movendo tanto o reducer quanto o contexto para um único arquivo. Atualmente, TasksContext.js
contém apenas duas declarações de contexto:
import { createContext } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
Esse arquivo vai ficar lotado! Você moverá o reducer para esse mesmo arquivo. Então, você declarará um novo componente TasksProvider
no mesmo arquivo. Este componente juntará todas as peças:
- Ele gerenciará o estado com um reducer.
- Ele fornecerá ambos os contextos para componentes abaixo.
- Ele receberá
children
como uma prop para que você possa passar JSX para ele.
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
Isso remove toda a complexidade e a ligação do seu componente TaskApp
:
import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; import { TasksProvider } from './TasksContext.js'; export default function TaskApp() { return ( <TasksProvider> <h1>Dia de folga em Kyoto</h1> <AddTask /> <TaskList /> </TasksProvider> ); }
Você também pode exportar funções que usam o contexto de TasksContext.js
:
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
Quando um componente precisa ler o contexto, ele pode fazê-lo através dessas funções:
const tasks = useTasks();
const dispatch = useTasksDispatch();
Isso não altera o comportamento de forma alguma, mas permite que você separe ainda mais esses contextos ou adicione alguma lógica a essas funções mais tarde. Agora toda a ligação de contexto e reducer está em TasksContext.js
. Isso mantém os componentes limpos e sem complicações, focados no que exibem em vez de onde obtêm os dados:
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)}> Salvar </button> </> ); } else { taskContent = ( <> {task.text} <button onClick={() => setIsEditing(true)}> Editar </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 }); }}> Deletar </button> </label> ); }
Você pode pensar em TasksProvider
como uma parte da tela que sabe como lidar com tarefas, useTasks
como uma maneira de lê-las, e useTasksDispatch
como uma maneira de atualizá-las de qualquer componente abaixo na árvore.
À medida que seu aplicativo cresce, você pode ter muitos pares de contexto-reducer como este. Essa é uma maneira poderosa de escalar seu aplicativo e elevar o estado sem muito trabalho sempre que você quiser acessar os dados profundamente na árvore.
Recap
- Você pode combinar reducer com contexto para permitir que qualquer componente leia e atualize o estado acima dele.
- Para fornecer estado e a função dispatch para componentes abaixo:
- Crie dois contextos (um para estado e outro para funções de dispatch).
- Forneça ambos os contextos do componente que usa o reducer.
- Use qualquer contexto a partir dos componentes que precisam lê-los.
- Você pode ainda mais limpar os componentes movendo toda a ligação para um arquivo.
- Você pode exportar um componente como
TasksProvider
que fornece contexto. - Você também pode exportar Hooks personalizados como
useTasks
euseTasksDispatch
para lê-lo.
- Você pode exportar um componente como
- Você pode ter muitos pares de contexto-reducer como este em seu aplicativo.