Mở rộng quy mô với Reducer và Context

Reducer cho phép bạn hợp nhất logic cập nhật state của component. Context cho phép bạn truyền thông tin xuống sâu tới các component khác. Bạn có thể kết hợp reducer và context lại với nhau để quản lý state của một màn hình phức tạp.

Bạn sẽ được học

  • Cách kết hợp reducer với context
  • Cách tránh truyền state và dispatch qua props
  • Cách giữ logic context và state trong một file riêng biệt

Kết hợp reducer với context

Trong ví dụ này từ phần giới thiệu về reducer, state được quản lý bởi một reducer. Function reducer chứa tất cả logic cập nhật state và được khai báo ở cuối file này:

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

Một reducer giúp các event handler ngắn gọn và súc tích. Tuy nhiên, khi ứng dụng của bạn phát triển, bạn có thể gặp phải một khó khăn khác. Hiện tại, tasks state và function dispatch chỉ có sẵn trong component cấp cao nhất TaskApp. Để cho phép các component khác đọc danh sách task hoặc thay đổi nó, bạn phải một cách rõ ràng truyền xuống state hiện tại và các event handler thay đổi nó dưới dạng props.

Ví dụ, TaskApp truyền danh sách task và các event handler tới TaskList:

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

TaskList truyền các event handler tới Task:

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

Trong một ví dụ nhỏ như thế này, điều này hoạt động tốt, nhưng nếu bạn có hàng chục hoặc hàng trăm component ở giữa, việc truyền xuống tất cả state và function có thể khá bực bội!

Đây là lý do tại sao, như một giải pháp thay thế cho việc truyền chúng qua props, bạn có thể muốn đặt cả tasks state và function dispatch vào context. Bằng cách này, bất kỳ component nào bên dưới TaskApp trong cây có thể đọc task và dispatch action mà không cần “prop drilling” lặp đi lặp lại.

Đây là cách bạn có thể kết hợp reducer với context:

  1. Tạo context.
  2. Đặt state và dispatch vào context.
  3. Use context ở bất kỳ đâu trong cây.

Bước 1: Tạo context

Hook useReducer trả về tasks hiện tại và function dispatch cho phép bạn cập nhật chúng:

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

Để truyền chúng xuống cây, bạn sẽ tạo hai context riêng biệt:

  • TasksContext cung cấp danh sách task hiện tại.
  • TasksDispatchContext cung cấp function cho phép các component dispatch action.

Export chúng từ một file riêng biệt để sau này bạn có thể import chúng từ các file khác:

import { createContext } from 'react';

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

Ở đây, bạn đang truyền null làm giá trị mặc định cho cả hai context. Các giá trị thực tế sẽ được cung cấp bởi component TaskApp.

Bước 2: Đặt state và dispatch vào context

Bây giờ bạn có thể import cả hai context trong component TaskApp của mình. Lấy tasksdispatch được trả về bởi useReducer()cung cấp chúng cho toàn bộ cây bên dưới:

import { TasksContext, TasksDispatchContext } from './TasksContext.js';

export default function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
// ...
return (
<TasksContext value={tasks}>
<TasksDispatchContext value={dispatch}>
...
</TasksDispatchContext>
</TasksContext>
);
}

Hiện tại, bạn truyền thông tin cả qua props và trong 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 value={tasks}>
      <TasksDispatchContext value={dispatch}>
        <h1>Day off in Kyoto</h1>
        <AddTask
          onAddTask={handleAddTask}
        />
        <TaskList
          tasks={tasks}
          onChangeTask={handleChangeTask}
          onDeleteTask={handleDeleteTask}
        />
      </TasksDispatchContext>
    </TasksContext>
  );
}

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

Trong bước tiếp theo, bạn sẽ loại bỏ việc truyền props.

Bước 3: Use context ở bất kỳ đâu trong cây

Bây giờ bạn không cần truyền danh sách task hoặc các event handler xuống cây:

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

Thay vào đó, bất kỳ component nào cần danh sách task có thể đọc nó từ TaskContext:

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

Để cập nhật danh sách task, bất kỳ component nào có thể đọc function dispatch từ context và gọi nó:

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>
// ...

Component TaskApp không truyền bất kỳ event handler nào xuống, và TaskList cũng không truyền bất kỳ event handler nào tới component Task. Mỗi component đọc context mà nó cần:

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

State vẫn “sống” trong component cấp cao nhất TaskApp, được quản lý bằng useReducer. Nhưng tasksdispatch của nó giờ đây có sẵn cho mọi component bên dưới trong cây bằng cách import và use những context này.

Chuyển tất cả wiring vào một file duy nhất

Bạn không bắt buộc phải làm điều này, nhưng bạn có thể làm gọn gàng hơn các component bằng cách chuyển cả reducer và context vào một file duy nhất. Hiện tại, TasksContext.js chỉ chứa hai khai báo context:

import { createContext } from 'react';

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

File này sắp trở nên đông đúc! Bạn sẽ chuyển reducer vào cùng file đó. Sau đó bạn sẽ khai báo một component TasksProvider mới trong cùng file. Component này sẽ kết nối tất cả các phần lại với nhau:

  1. Nó sẽ quản lý state bằng reducer.
  2. Nó sẽ cung cấp cả hai context cho các component bên dưới.
  3. Nó sẽ nhận children làm prop để bạn có thể truyền JSX vào nó.
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

return (
<TasksContext value={tasks}>
<TasksDispatchContext value={dispatch}>
{children}
</TasksDispatchContext>
</TasksContext>
);
}

Điều này loại bỏ tất cả sự phức tạp và wiring khỏi component TaskApp của bạn:

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

Bạn cũng có thể export những function use context từ TasksContext.js:

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

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

Khi một component cần đọc context, nó có thể làm điều đó thông qua những function này:

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

Điều này không thay đổi hành vi theo bất kỳ cách nào, nhưng nó cho phép bạn sau này tách những context này thêm hoặc thêm một số logic vào những function này. Bây giờ tất cả wiring context và reducer đều nằm trong TasksContext.js. Điều này giữ cho các component sạch sẽ và gọn gàng, tập trung vào những gì chúng hiển thị hơn là nơi chúng lấy dữ liệu:

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

Bạn có thể nghĩ về TasksProvider như một phần của màn hình biết cách xử lý task, useTasks như một cách để đọc chúng, và useTasksDispatch như một cách để cập nhật chúng từ bất kỳ component nào bên dưới trong cây.

Note

Những function như useTasksuseTasksDispatch được gọi là Custom Hooks. Function của bạn được coi là Custom Hook nếu tên của nó bắt đầu bằng use. Điều này cho phép bạn use các Hook khác, như useContext, bên trong nó.

Khi ứng dụng của bạn phát triển, bạn có thể có nhiều cặp context-reducer như thế này. Đây là một cách mạnh mẽ để mở rộng quy mô ứng dụng của bạn và nâng state lên mà không cần quá nhiều công việc bất cứ khi nào bạn muốn truy cập dữ liệu sâu trong cây.

Tóm tắt

  • Bạn có thể kết hợp reducer với context để cho phép bất kỳ component nào đọc và cập nhật state phía trên nó.
  • Để cung cấp state và function dispatch cho các component bên dưới:
    1. Tạo hai context (cho state và cho dispatch function).
    2. Cung cấp cả hai context từ component use reducer.
    3. Use context từ các component cần đọc chúng.
  • Bạn có thể làm gọn gàng hơn các component bằng cách chuyển tất cả wiring vào một file.
    • Bạn có thể export một component như TasksProvider để cung cấp context.
    • Bạn cũng có thể export các Custom Hook như useTasksuseTasksDispatch để đọc nó.
  • Bạn có thể có nhiều cặp context-reducer như thế này trong ứng dụng của mình.