使用 Reducer 和 Context 擴展應用程式

Reducers 讓你可以整合元件的狀態更新邏輯。Context 讓你將資訊深入傳遞給其他元件。你可以將 Reducers 和 Context 結合起來管理複雜螢幕的狀態。

你將學習到

  • 如何將 Reducer 與 Context 結合
  • 如何避免透過 props 傳遞狀態和 dispatch
  • 如何在單獨的檔案中維護 Context 和狀態邏輯

將 Reducer 與 Context 結合

在這個來自 Reducer 簡介 的範例中,狀態是由 Reducer 管理的。Reducer 函式包含所有狀態更新邏輯,並在此檔案的底部宣告

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 }
];

Reducer 有助於保持事件處理程式簡潔明瞭。然而,隨著應用程式的增長,你可能會遇到另一個困難。**目前,`tasks` 狀態和 `dispatch` 函式僅在頂層的 `TaskApp` 元件中可用。** 為了讓其他元件讀取任務列表或更改它,你必須明確地**將**目前的狀態和更改它的事件處理程式作為 props 傳遞下去。

例如,`TaskApp` 將任務列表和事件處理程式傳遞給 `TaskList`

<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>

而 `TaskList` 將事件處理程式傳遞給 `Task`

<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/>

在像這樣的小範例中,這樣做效果很好,但是如果中間有數十或數百個元件,傳遞所有狀態和函式可能會非常令人沮喪!

這就是為什麼,作為透過 props 傳遞它們的替代方案,你可能希望將 `tasks` 狀態和 `dispatch` 函式**放入 Context 中。** **這樣,樹狀結構中 `TaskApp` 下方的任何元件都可以讀取任務並發送動作,而無需重複的「prop drilling」。**

以下是將 Reducer 與 Context 結合的方法

  1. **建立** Context。
  2. **將** 狀態和 dispatch 放入 Context 中。
  3. **在** 樹狀結構中的任何位置使用 Context。

步驟 1:建立 Context

`useReducer` Hook 會返回目前的 `tasks` 和讓你更新它們的 `dispatch` 函式

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

為了將它們傳遞下去,你將**建立**兩個單獨的 Context

  • `TasksContext` 提供目前的任務列表。
  • `TasksDispatchContext` 提供允許元件發送動作的函式。

將它們從單獨的檔案中匯出,以便稍後可以從其他檔案中匯入它們

import { createContext } from 'react';

export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);

在這裡,你將 `null` 作為預設值傳遞給兩個 Context。實際值將由 `TaskApp` 元件提供。

步驟 2:將狀態和 dispatch 放入 context 中

現在,您可以在您的 TaskApp 元件中導入兩個 context。使用 useReducer() 返回的 tasksdispatch,並將它們提供給下方的整個元件樹。

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>
);
}

目前,您同時透過 props 和 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 }
];

在下一步中,您將移除 props 傳遞。

步驟 3:在元件樹中的任何位置使用 context

現在您不需要將任務列表或事件處理函式向下傳遞到元件樹了。

<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
<h1>Day off in Kyoto</h1>
<AddTask />
<TaskList />
</TasksDispatchContext.Provider>
</TasksContext.Provider>

任何需要任務列表的元件都可以從 TaskContext 中讀取它。

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

要更新任務列表,任何元件都可以從 context 中讀取 dispatch 函式並呼叫它。

export default function AddTask() {
const [text, setText] = useState('');
const dispatch = useContext(TasksDispatchContext);
// ...
return (
// ...
<button onClick={() => {
setText('');
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}}>Add</button>
// ...

TaskApp 元件不會向下傳遞任何事件處理函式,TaskList 也不會將任何事件處理函式傳遞給 Task 元件。每個元件都會讀取它需要的 context。

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)}>
          Save
        </button>
      </>
    );
  } else {
    taskContent = (
      <>
        {task.text}
        <button onClick={() => setIsEditing(true)}>
          Edit
        </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
        });
      }}>
        Delete
      </button>
    </label>
  );
}

狀態仍然「存在」於頂層的 TaskApp 元件中,並使用 useReducer 進行管理。 但是它的 tasksdispatch 現在可以透過導入和使用這些 context,讓樹狀結構下方的每個元件都能使用。

將所有連線移到單個檔案中

您不必這樣做,但您可以將 reducer 和 context 移到單個檔案中,以進一步簡化元件。目前,TasksContext.js 只包含兩個 context 宣告。

import { createContext } from 'react';

export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);

這個檔案快要塞滿了!您會將 reducer 移到同一個檔案中。然後,您將在同一個檔案中宣告一個新的 TasksProvider 元件。這個元件會將所有部分連結在一起。

  1. 它將使用 reducer 管理狀態。
  2. 它將為下方的元件提供兩個 context。
  3. 它將children 作為 prop 接收,以便您可以將 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>
);
}

這將從您的 TaskApp 元件中移除所有複雜性和連線。

import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import { TasksProvider } from './TasksContext.js';

export default function TaskApp() {
  return (
    <TasksProvider>
      <h1>Day off in Kyoto</h1>
      <AddTask />
      <TaskList />
    </TasksProvider>
  );
}

您還可以從 TasksContext.js 導出*使用* context 的函式。

export function useTasks() {
return useContext(TasksContext);
}

export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}

當元件需要讀取 context 時,它可以透過這些函式來完成。

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

這不會以任何方式改變行為,但它可以讓您稍後進一步拆分這些 context 或向這些函式新增一些邏輯。現在所有 context 和 reducer 的連線都在 TasksContext.js 中。這讓元件保持乾淨和整潔,專注於它們顯示的內容,而不是它們從哪裡獲取資料:

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)}>
          Save
        </button>
      </>
    );
  } else {
    taskContent = (
      <>
        {task.text}
        <button onClick={() => setIsEditing(true)}>
          Edit
        </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
        });
      }}>
        Delete
      </button>
    </label>
  );
}

您可以將 TasksProvider 視為螢幕的一部分,它知道如何處理任務;將 useTasks 視為讀取它們的方式;將 useTasksDispatch 視為從樹狀結構下方的任何元件更新它們的方式。

備註

useTasksuseTasksDispatch 這樣的函式稱為*自定義 Hooks*。如果您的函式名稱以 use 開頭,則它被視為自定義 Hook。這讓您可以在其中使用其他 Hooks,例如 useContext

隨著應用程式的增長,您可能會有許多像這樣的 context-reducer 對。這是一種強大的方法,可以擴展您的應用程式,並在您想要深入存取樹狀結構中的資料時,在不需太多工作的情況下將狀態提升

回顧

  • 您可以將 reducer 與 context 結合,讓任何元件都能讀取和更新其上方的狀態。
  • 要將狀態和 dispatch 函式提供給下方的元件:
    1. 創建兩個 context(一個用於狀態,一個用於 dispatch 函式)。
    2. 從使用 reducer 的元件提供兩個 context。
    3. 從需要讀取它們的元件使用任一 context。
  • 您可以將所有連線移到一個檔案中,以進一步簡化元件。
    • 您可以導出一個像 TasksProvider 這樣的元件來提供 context。
    • 您還可以導出像 useTasksuseTasksDispatch 這樣的自定義 Hooks 來讀取它。
  • 您的應用程式中可以有許多像這樣的 context-reducer 對。