Context

Xét một ứng dụng quản lý task sử dụng reducer:

function TaskApp() {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks)
 
  // Event handlers ...
 
  return (
    <>
      <h1>Day off in Kyoto</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} />
    </>
  )
}
 
// Reducer and initial tasks ...

Để có thể truyền state tasks và hàm dispatch xuống cho các child component mà không thông qua props drilling1, ta có thể kết hợp với context.

Create the Context

Tạo ra hai context để lưu state và dispatch function ở trong một file riêng biệt:

import { createContext } from "react"
 
export const TasksContext = createContext(null)
export const TasksDispatchContext = createContext(null)

Put State and Dispatch into Context

Cung cấp context cho các child component:

// Existing import ...
import { TasksContext, TasksDispatchContext } from "./TasksContext.js"
 
function TaskApp() {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks)
 
  // Event handlers ...
 
  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>
  )
}
 
// Reducer and initial tasks ...

Use Context Anywhere in the Tree

Loại bỏ các props passing khỏi các child component:

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

Thay vào đó, child component có thể lấy state từ context:

// Existing import ...
import { TasksContext } from "./TasksContext.js"
 
function TaskList() {
  const tasks = useContext(TasksContext)
  // ...
}

Hoặc dùng dispatch function từ context để cập nhật danh sách các task:

// Existing import ...
import { TasksDispatchContext } from "./TasksContext.js"
 
function AddTask() {
  const [text, setText] = useState("")
  const dispatch = useContext(TasksDispatchContext)
  // ...
  return (
    // ...
    <button
      onClick={() => {
        setText("")
        dispatch({ type: "added", id: nextId++, text: text })
      }}
    >
      Add
    </button>
    // ...
  )
}

Có thể thấy, khi sử dụng context, các event handler sẽ được từng child component quản lý thay vì được quản lý bởi parent component như trước.

Moving All Wiring into a Single File

Chúng ta nên đặt các context và reducer ở trong một file cũng như là tạo một context provider như sau:

// TasksContext.jsx
import { createContext } from "react"
 
export const TasksContext = createContext(null)
export const TasksDispatchContext = createContext(null)
 
export function TasksProvider({ children }) {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks)
 
  return (
    <TasksContext.Provider value={tasks}>
      <TasksDispatchContext.Provider value={dispatch}>{children}</TasksDispatchContext.Provider>
    </TasksContext.Provider>
  )
}
 
// Reducer and initial tasks ...

Bằng cách này, component sử dụng reducer và context (TaskApp) sẽ được giảm độ phức tạp đi đáng kể:

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

Ngoài ra, ta cũng có thể tạo các custom hook cho phép lấy ra các thành phần của context:

// TasksContext.jsx
export function useTasks() {
  return useContext(TasksContext)
}
 
export function useTasksDispatch() {
  return useContext(TasksDispatchContext)
}

Các child component có thể gọi các hook này như sau:

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

Resources

Footnotes

  1. Truyền props nhiều lần từ parent component đến các child component hoặc leaf component. Xem thêm Context.