Escalando com Reducer e Context

Reducers permitem que você consolide a lógica de atualização de estado de um componente. Context permite que você passe informações para outros componentes. Você pode combinar reducers e context juntos para gerenciar o estado de uma tela complexa.

Você aprenderá

  • Como combinar um reducer com context
  • Como evitar passar estado e dispatch através de props
  • Como manter a lógica de context e estado em um arquivo separado

Combinando um reducer com context

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 do 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>Day off in 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: 'Philosopher’s Path', done: true },
  { id: 1, text: 'Visit the temple', done: false },
  { id: 2, text: 'Drink 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 TaskApp de nível superior. Para permitir que outros componentes leiam a lista de tarefas ou a alterem, você tem que passar explicitamente 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 todo o estado e as funções pode ser bastante frustrante!

É por isso que, como alternativa para passá-los por props, você pode querer colocar o estado tasks e a função dispatch em context. Desta forma, qualquer componente abaixo de TaskApp na árvore pode ler as tarefas e despachar ações sem a repetitiva “prop drilling”.

Veja como você pode combinar um reducer com context:

  1. Crie o context.
  2. Coloque o estado e dispatch no context.
  3. Use o context em qualquer lugar na árvore.

Etapa 1: Crie o context

O Hook useReducer retorna as tasks atuais e a função dispatch que permite atualizá-las:

const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

Para passá-los pela árvore, você irá criar dois contextos separados:

  • TasksContext fornece a lista atual de tarefas.
  • TasksDispatchContext fornece a função que permite que os componentes despachem ações.

Exporte-os de um arquivo separado para que você possa importá-los mais tarde 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.

Etapa 2: Coloque o estado e dispatch no context

Agora você pode importar ambos os contextos no seu componente TaskApp. Pegue as tasks e dispatch retornadas por useReducer() e forneça-as 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 em 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>Day off in 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: 'Philosopher’s Path', done: true },
  { id: 1, text: 'Visit the temple', done: false },
  { id: 2, text: 'Drink matcha', done: false }
];

Na próxima etapa, você removerá a passagem de props.

Etapa 3: Use o context 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>Day off in Kyoto</h1>
<AddTask />
<TaskList />
</TasksDispatchContext.Provider>
</TasksContext.Provider>

Em vez disso, qualquer componente que precise da lista de tarefas pode lê-la do TaskContext:

export default function TaskList() {
const tasks = useContext(TasksContext);
// ...

Para atualizar a lista de tarefas, qualquer componente pode ler a função dispatch do context 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 e TaskList também não passa nenhum manipulador de eventos para o componente Task. Cada componente lê o contexto 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
        });
      }}>
        Apagar
      </button>
    </label>
  );
}

O state ainda “vive” no componente TaskApp de nível superior, gerenciado com useReducer. Mas seus tasks e dispatch agora estão disponíveis para cada componente abaixo na árvore importando e usando esses contextos.

Mover toda a fiação para um único arquivo

Você não precisa fazer isso, mas você pode remover mais a desordem dos componentes, movendo tanto o redutor 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);

Este arquivo está prestes a ficar lotado! Você moverá o redutor para o mesmo arquivo. Então você declarará um novo componente TasksProvider no mesmo arquivo. Este componente irá unir todas as peças:

  1. Ele irá gerenciar o estado com um redutor.
  2. Ele irá fornecer ambos os contextos para componentes abaixo.
  3. Ele irá 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 fiaçã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>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 por meio dessas funções:

const tasks = useTasks();
const dispatch = useTasksDispatch();

Isso não altera o comportamento de forma alguma, mas permite que você divida esses contextos posteriormente ou adicione alguma lógica a essas funções. Agora, toda a fiação do contexto e do redutor está em TasksContext.js. Isso mantém os componentes limpos e organizados, focados no que eles exibem e não 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
        });
      }}>
        Apagar
      </button>
    </label>
  );
}

Você pode pensar no TasksProvider como uma parte da tela que sabe como lidar com tarefas, no useTasks como uma maneira de lê-las, e no useTasksDispatch como uma maneira de atualizá-las de qualquer componente abaixo na árvore.

Note

Funções como useTasks e useTasksDispatch são chamadas de Custom Hooks. Sua função é considerada um custom Hook se seu nome começar com use. Isso permite que você use outros Hooks, como useContext, dentro dele.

Conforme seu aplicativo cresce, você pode ter muitos pares de contexto-redutor como este. Esta é uma maneira poderosa de dimensionar seu aplicativo e elevar o estado sem muito trabalho sempre que você quiser acessar os dados no fundo da árvore.

Recap

  • Você pode combinar um redutor com contexto para permitir que qualquer componente leia e atualize o estado acima dele.
  • Para fornecer o estado e a função de envio para os componentes abaixo:
    1. Crie dois contextos (para estado e para funções de envio).
    2. Forneça ambos os contextos do componente que usa o redutor.
    3. Use qualquer um dos contextos de componentes que precisam lê-los.
  • Você pode remover ainda mais a desordem dos componentes movendo toda a fiação para um arquivo.
    • Você pode exportar um componente como TasksProvider que fornece o contexto.
    • Você também pode exportar custom Hooks como useTasks e useTasksDispatch para lê-lo.
  • Você pode ter muitos pares de contexto-redutor como este em seu aplicativo.