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

A reducer helps keep the event handlers short and concise. However, as your app grows, you might run into another difficulty. Currently, the tasks state and the dispatch function are only available in the top-level TaskApp component. To let other components read the list of tasks or change it, you have to explicitly pass down the current state and the event handlers that change it as props. 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 meneruskan 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 tree dapat membaca tugas dan melakukan aksi dispatch tanpa “pengeboran props” yang berulang.

Berikut adalah cara menggabungkan reducer dengan conteks:

  1. Buatlah context.
  2. Letakkan state dan dispatch ke dalam context.
  3. 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 penyebaran prop.

Langkah 3: Gunakan context di mana saja dalam tree

Now you don’t need to pass the list of tasks or the event handlers down the 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:

  1. Ia akan mengelola state dengan reducer.
  2. Ia akan menyediakan kedua context ke komponen di bawahnya.
  3. Ia akan mengambil children sebagai prop sehingga Anda dapat melewatkan 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.

Note

Fungsi-fungsi seperti useTasks dan useTasksDispatch disebut dengan Custom Hooks. Fungsi Anda dianggap sebagai custom Hook jika namanya dimulai dengan use. Ini memungkinkan Anda menggunakan Hooks lain, seperti useContext, di dalamnya.

Saat aplikasi Anda berkembang, 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:
    1. Buat dua context (untuk state dan untuk fungsi dispatch).
    2. Sediakan kedua context dari komponen yang menggunakan reducer.
    3. 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 dan useTasksDispatch untuk membacanya.
  • Anda dapat memiliki banyak pasangan context-reducer seperti ini di aplikasi Anda.