Peningkatan Skala dengan Reducer dan Context
Reducer memungkinkan Anda untuk konsolidasi logika pembaruan state komponen. Context memungkinkan Anda untuk mengirim informasi ke komponen lain yang lebih dalam. Anda dapat menggabungkan reducer dan context bersama-sama untuk mengelola state layar yang kompleks.
You will learn
- Bagaimana menggabungkan reducer dengan context
- Bagaimana menghindari mengoper state dan dispatch melalui props
- Bagaimana menjaga konteks dan logika state pada file terpisah
Menggabungkan reducer dengan context
Pada contoh dari pengenalan reducer, state dikelola oleh reducer. Fungsi reducer berisi semua logika pembaruan state dan dinyatakan di bagian bawah file ini:
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 membantu menjaga event handlers menjadi singkat dan ringkas. Namun, ketika aplikasi Anda berkembang, Anda mungkin akan menemukan kesulitan lain. Saat ini, state tugas
dan fungsi dispatch
hanya tersedia di komponen TaskApp
level atas. Untuk memungkinkan komponen lain membaca daftar tugas atau mengubahnya, Anda harus secara eksplisit meneruskan state saat ini dan event handlers yang mengubahnya sebagai props.
Misalnya, TaskApp
meneruskan daftar tugas dan event handlers ke TaskList
:
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
Dan TaskList
mengoper event handlers ke Task
:
<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/>
Dalam contoh kecil seperti ini, cara ini dapat berfungsi dengan baik, namun jika Anda memiliki puluhan atau ratusan komponen di tengah, meneruskan semua state dan fungsi dapat sangat menjengkelkan!
Inilah mengapa, sebagai alternatif untuk melewatkan melalui props, Anda mungkin ingin menempatkan baik state tugas
maupun fungsi dispatch
ke dalam context . Dengan cara ini, komponen apa pun di bawah TaskApp
dalam pohon(tree) dapat membaca tugas dan melakukan aksi dispatch tanpa “prop drilling” yang berulang.
Berikut adalah cara menggabungkan reducer dengan conteks:
- Buatlah context.
- Letakkan state dan dispatch ke dalam context.
- Gunakan context di mana saja dalam tree.
Langkah 1: Buat conteks
Hook useReducer
mengembalikan tugas
saat ini dan fungsi dispatch
yang memungkinkan Anda memperbarui tugas:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
Untuk meneruskannya ke dalam tree, Anda akan membuat dua contexts terpisah:
TasksContext
menyediakan daftar tugas saat ini.TasksDispatchContext
menyediakan fungsi yang memungkinkan komponen melakukan aksi dispatch.
Kemudian ekspor keduanya dari file terpisah agar nantinya dapat diimpor dari file lain:
import { createContext } from 'react'; export const TasksContext = createContext(null); export const TasksDispatchContext = createContext(null);
Di sini, Anda meneruskan null
sebagai nilai default ke kedua context. Nilai aktual akan disediakan oleh komponen TaskApp
.
Langkah 2: Letakkan state dan dispatch ke dalam context
Sekarang Anda dapat mengimpor kedua context di komponen TaskApp
Anda. Ambil tugas
dan dispatch
yang dikembalikan oleh useReducer()
dan sediakan mereka ke seluruh tree di bawah:
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>
);
}
Saat ini, Anda meneruskan informasi baik melalui props maupun melalui 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 } ];
Pada langkah selanjutnya, Anda akan menghapus pengoperan prop.
Langkah 3: Gunakan context di mana saja dalam tree
Sekarang Anda tidak perlu lagi meneruskan daftar tugas atau event handler ke bawah tree:
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
<h1>Day off in Kyoto</h1>
<AddTask />
<TaskList />
</TasksDispatchContext.Provider>
</TasksContext.Provider>
Sebaliknya, komponen mana pun yang memerlukan daftar tugas dapat membacanya dari TaskContext
:
export default function TaskList() {
const tasks = useContext(TasksContext);
// ...
Untuk memperbarui daftar tugas, komponen mana pun dapat membaca fungsi dispatch
dari context dan memanggilnya:
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>
// ...
Komponen TaskApp
tidak meneruskan event handler ke bawah, dan TaskList
juga tidak meneruskan event handler ke komponen Task
. Setiap komponen membaca context yang dibutuhkannya:
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 masih “berada” di dalam komponen TaskApp
level atas, dikelola dengan useReducer
. Tetapi daftar tugas
dan fungsi dispatch
sekarang tersedia untuk setiap komponen di bawahnya dalam tree dengan mengimpor dan menggunakan context tersebut.
Memindahkan semua penghubung ke satu file
Anda tidak harus melakukannya, tetapi Anda dapat membersihkan komponen dengan memindahkan reducer dan context ke dalam satu file. Saat ini, TasksContext.js
hanya berisi dua deklarasi context:
import { createContext } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
File ini akan semakin ramai! Anda akan memindahkan reducer ke dalam file yang sama. Kemudian Anda akan mendeklarasikan komponen TasksProvider
baru dalam file yang sama. Komponen ini akan mengikat semua bagian bersama-sama:
- Ia akan mengelola state dengan reducer.
- Ia akan menyediakan kedua context ke komponen di bawahnya.
- Ia akan mengambil children sebagai prop sehingga Anda dapat mengoper JSX padanya.
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
Ini menghilangkan semua kompleksitas dan penghubung dari komponen TaskApp
Anda:
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> ); }
Anda juga dapat mengekspor fungsi-fungsi yang menggunakan context dari TasksContext.js
:
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
Ketika sebuah komponen perlu membaca context, dapat dilakukan melalui fungsi-fungsi ini:
const tasks = useTasks();
const dispatch = useTasksDispatch();
Hal ini tidak mengubah perilaku secara apa pun, tetapi memungkinkan Anda untuk memisahkan context ini lebih lanjut atau menambahkan beberapa logika ke fungsi-fungsi ini. Sekarang semua pengaturan context dan reducer ada di TasksContext.js
. Ini menjaga komponen tetap bersih dan tidak berantakan, fokus pada apa yang mereka tampilkan daripada dari mana mereka mendapatkan data:
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> ); }
You can think of TasksProvider
as a part of the screen that knows how to deal with tasks, useTasks
as a way to read them, and useTasksDispatch
as a way to update them from any component below in the tree.
Anda dapat memandang TasksProvider
sebagai bagian dari layar yang tahu cara menangani tugas, useTasks
sebagai cara untuk membacanya, dan useTasksDispatch
sebagai cara untuk memperbaruinya dari komponen mana pun di bawah tree.
Seiring dengan pertumbuhan aplikasi Anda, mungkin Anda akan memiliki banyak pasangan context-reducer seperti ini. Ini adalah cara yang kuat untuk meningkatkan aplikasi Anda dan mengangkat state ke atas tanpa terlalu banyak pekerjaan setiap kali Anda ingin mengakses data yang dalam di dalam tree.
Recap
- Anda dapat menggabungkan reducer dengan context untuk memungkinkan komponen mana pun membaca dan memperbarui state di atasnya.
- Untuk menyediakan state dan fungsi dispatch ke komponen di bawah:
- Buat dua context (untuk state dan untuk fungsi dispatch).
- Sediakan kedua context dari komponen yang menggunakan reducer.
- Gunakan salah satu context dari komponen yang perlu membacanya.
- Anda dapat memindahkan seluruh penghubung ke satu file untuk memperjelas komponen.
- Anda dapat mengekspor komponen seperti
TasksProvider
yang menyediakan context. - Anda juga dapat mengekspor Custom Hooks seperti
useTasks
danuseTasksDispatch
untuk membacanya.
- Anda dapat mengekspor komponen seperti
- Anda dapat memiliki banyak pasangan context-reducer seperti ini di aplikasi Anda.