Peningkatan Skala dengan Reducer dan Context

Reducer memungkinkan Anda untuk mengonsolidasi 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 tasks 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:

  1. Buatlah context.
  2. Letakkan state dan dispatch ke dalam context.
  3. Gunakan context di mana saja dalam tree.

Langkah 1: Buat context

Hook useReducer mengembalikan tasks saat ini dan fungsi dispatch yang memungkinkan Anda memperbaruinya:

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

Untuk meneruskannya ke dalam tree, Anda akan membuat dua context 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 tasks dan dispatch yang dikembalikan oleh useReducer() dan sediakan mereka kepada seluruh pohon (tree) di bawahnya:

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 pohon

Sekarang Anda tidak perlu lagi meneruskan daftar tugas atau event handler ke bawah pohon:

<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 tasks dan fungsi dispatch sekarang tersedia untuk setiap komponen di bawah pohon tersebut 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 mengoper JSX kepadanya.
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>
  );
}

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

Note

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

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 pohon (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 Hooks Custom seperti useTasks dan useTasksDispatch untuk membacanya.
  • Anda dapat memiliki banyak pasangan context-reducer seperti ini di aplikasi Anda.