Mengekstraksi Logika State ke Reducer
Komponen dengan banyak pembaruan state yang tersebar di banyak event handlers bisa menjadi sangat membingungkan. Untuk kasus seperti ini, Anda dapat menggabungkan semua logika pembaruan state di luar komponen Anda dalam satu fungsi, yang disebut sebagai reducer.
Anda akan mempelajari
- Apa itu fungsi reducer
- Bagaimana cara untuk migrasi dari fungsi
useState
menjadiuseReducer
- Kapan menggunakan fungsi reducer
- Bagaimana cara menulis fungsi reducer dengan baik
Mengkonsolidasikan logika state menggunakan reducer
Saat komponen-komponen Anda semakin kompleks, hal ini mengakibatkan berbagai cara memperbarui state komponen dalam kode menjadi sulit untuk dilihat secara sekilas. Contoh, komponen TaskApp
di bawah menyimpan senarai tasks
dalam state dan menggunakan tiga handler untuk menambahkan, menghapus dan mengubah tasks
:
import { useState } from 'react'; import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; export default function TaskApp() { const [tasks, setTasks] = useState(initialTasks); function handleAddTask(text) { setTasks([ ...tasks, { id: nextId++, text: text, done: false, }, ]); } function handleChangeTask(task) { setTasks( tasks.map((t) => { if (t.id === task.id) { return task; } else { return t; } }) ); } function handleDeleteTask(taskId) { setTasks(tasks.filter((t) => t.id !== taskId)); } return ( <> <h1>Prague itinerary</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </> ); } let nextId = 3; const initialTasks = [ {id: 0, text: 'Mengunjungi Museum Kafka', done: true}, {id: 1, text: 'Tonton pertunjukan boneka', done: false}, {id: 2, text: 'Foto Lennon Wall', done: false}, ];
Setiap event handler pada komponen TaskApp
akan memangil fungsi setTask
untuk melakukan pembaharuan state. Saat komponen ini bertumbuh semakin besar, logika state akan semakin rumit. Untuk mengurangi kompleksitas dan menyimpan semua logika Anda di satu tempat yang mudah diakses. Anda dapat memindahkan logika state tersebut ke sebuah fungsi di luar komponen Anda, yang disebut “reducer”.
Reducer merupakan sebuah alternatif untuk menangani state. Anda dapat migrasi dari useState
ke useReducer
dalam tiga langkah:
- Pindah dari menyetel state menjadi men-dispatch action
- Tulis fungsi reducer.
- Gunakan reducer pada komponen Anda.
Langkah 1: Pindah dari menyetel state menjadi men-dispatch action
Event handler Anda sekarang menentukan apa yang harus dilakukan dengan menyetel state:
function handleAddTask(text) {
setTasks([
...tasks,
{
id: nextId++,
text: text,
done: false,
},
]);
}
function handleChangeTask(task) {
setTasks(
tasks.map((t) => {
if (t.id === task.id) {
return task;
} else {
return t;
}
})
);
}
function handleDeleteTask(taskId) {
setTasks(tasks.filter((t) => t.id !== taskId));
}
Hapus semua logika untuk mengatur state. Maka yang tersisa adalah tiga event handlers:
handleAddTask(text)
akan dipanggil ketika pengguna menekan tombol “Add”.handleChangeTask(task)
akan dipanggil ketika pengguna matikan sebuah task atau menekan tombol “Save”.handleDeleteTask(taskId)
akan dipanggil ketika pengguna menekan tombol “Delete”.
Mengelola state dengan fungsi reducer akan sedikit berbeda dari memanggil fungsi penetap state secara langsung. Alih-alih memberi tahu React “apa yang harus dilakukan” dengan menetapkan state, Anda merincis “apa yang baru saja dilakukan pengguna” dengan mengirim “aksi” dari event handler Anda. (Logika pembaruan state akan berada di tempat lain!) Jadi, alih-alih “mengatur tasks
” melalui event handler, Anda mengirim aksi “menambahkan/mengubah/menghapus task”. Cara ini lebih deskriptif untuk mengetahui intensi aksi pengguna.
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,
});
}
Objek yang anda masukan ke dispatch
dapat disebut sebagai “aksi”:
function handleDeleteTask(taskId) {
dispatch(
// objek "aksi":
{
type: 'deleted',
id: taskId,
}
);
}
Contoh diatas merupakan objek JavaScript biasa. Anda dapat menentukan objek apapun yang akan dimasukkan ke dalamnya, tetapi umumnya harus berisi sebuah objek yang setidaknya berisi informasi Apa yang terjadi. (Anda akan menambahkan fungsi dispatch
itu sendiri di langkah selanjutnya.)
Langkah 2: Tulis fungsi reducer
Fungsi reducer merupakan tempat dimana Anda meletakkan logika state. Fungsi reducer menerima dua argumen yaitu state sekarang dan object aksi, dan mengembalikan state berikutnya:
function yourReducer(state, action) {
// mengembalikan state berikutnya untuk ditetapkan oleh React
}
React akan menetapkan state yang dikembalikan oleh reducer Anda.
Untuk memindahkan logika fungsi penetap state dari event handler Anda menjadi fungsi reducer pada contoh berikut, Anda akan:
- Deklarasikan state(
tasks
) saat ini sebagai argumen pertama. - Deklarasikan objek
action
sebagai argumen kedua. - Mengembalikan state berikutnya melalui reducer (di mana React akan mengatur statusnya).
Berikut merupakan logika funsi penetap state yang dimigrasikan ke fungsi reducer:
function tasksReducer(tasks, action) {
if (action.type === 'added') {
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
},
];
} else if (action.type === 'changed') {
return tasks.map((t) => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
} else if (action.type === 'deleted') {
return tasks.filter((t) => t.id !== action.id);
} else {
throw Error('Unknown action: ' + action.type);
}
}
Karena fungsi reducer menerima state (tasks
) sebagai sebuah argument, Anda dapat mendeklarasikan fungsi reducer di luar komponen Anda. Dengan ini kode yang Anda tulis tingkat indentasinya akan berkurang dan akan lebih mudah dibaca.
Pendalaman
Meskipun reducer dapat “mengurangi” jumlah kode di dalam komponen Anda, sebenarnya mereka dinamakan setelah operasi reduce()
yang dapat dilakukan pada senarai.
Operasi reduce()
memungkinkan Anda untuk mengambil sebuah senarai dan “mengakumulasi” sebuah nilai tunggal dari banyak nilai:
const arr = [1, 2, 3, 4, 5];
const sum = arr.reduce(
(result, number) => result + number
); // 1 + 2 + 3 + 4 + 5
Fungsi yang Anda berikan ke reduce
dikenal sebagai “reducer”. Fungsi tersebut mengambil hasil sejauh ini dan item saat ini, kemudian mengembalikan hasil berikutnya. Reducer di React adalah contoh dari ide yang sama: mereka mengambil state sejauh ini dan aksi, lalu mengembalikan state berikutnya. Dengan cara ini, reducer mengakumulasi aksi dari waktu ke waktu ke dalam state.
Anda bahkan dapat menggunakan metode reduce()
dengan initialState
dan sebuah senarai aksi
untuk menghitung state akhir dengan melewatkan fungsi reducer ke dalamnya:
import tasksReducer from './tasksReducer.js'; let initialState = []; let actions = [ {type: 'added', id: 1, text: 'Visit Kafka Museum'}, {type: 'added', id: 2, text: 'Watch a puppet show'}, {type: 'deleted', id: 1}, {type: 'added', id: 3, text: 'Lennon Wall pic'}, ]; let finalState = actions.reduce(tasksReducer, initialState); const output = document.getElementById('output'); output.textContent = JSON.stringify(finalState, null, 2);
Anda mungkin tidak perlu melakukannya sendiri, tetapi ini mirip dengan apa yang dilakukan oleh React!
Langkah 3: Gunakan reducer pada komponen Anda
Terakhir, Anda perlu menghubungkan fungsi tasksReducer
ke komponen Anda. Impor fungsi hook useReducer
dari React:
import { useReducer } from 'react';
Kemudian Anda dapat mengganti fungsi useState
:
const [tasks, setTasks] = useState(initialTasks);
dengan fungsi useReducer
seperti ini:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
Fungsi hook useReducer
mirip dengan fungsi useState
—Anda harus melewatkan state awal ke dalamnya dan ia mengembalikan nilai stateful dan cara untuk mengatur state (dalam kasus ini, fungsi dispatch). Namun, sedikit berbeda.
Fungsi hook useReducer
mengambil dua argumen:
- Fungsi reducer.
- State awal.
Dan fungsi hook useReducer
mengembalikan:
- Nilai statefull.
- Fungsi dispatch (untuk “men-dispatch” aksi pengguna ke fungsi reducer).
Sekarang fungsi reducer sudah sepenuhnya terhubung! Di sini, fungsi reducer dinyatakan di bagian bawah file komponen:
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>Prague itinerary</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: 'Visit Kafka Museum', done: true}, {id: 1, text: 'Watch a puppet show', done: false}, {id: 2, text: 'Lennon Wall pic', done: false}, ];
Hal ini opsional, namun jika Anda ingin melakukannya, Anda bahkan dapat memindahkan reducer ke file yang berbeda:
import { useReducer } from 'react'; import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; import tasksReducer from './tasksReducer.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>Prague itinerary</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </> ); } let nextId = 3; const initialTasks = [ {id: 0, text: 'Visit Kafka Museum', done: true}, {id: 1, text: 'Watch a puppet show', done: false}, {id: 2, text: 'Lennon Wall pic', done: false}, ];
Logika komponen akan dapat lebih mudah dibaca ketika Anda memisahkan aspek seperti ini. Sekarang, event handler hanya menentukan apa yang terjadi dengan mengirimkan aksi, dan fungsi reducer menentukan bagaimana state diperbarui sebagai respons terhadapnya.
Membandingkan useState
dan useReducer
Reducers tidaklah tanpa kekurangan! Berikut adalah beberapa cara untuk membandingkannya:
- Ukuran kode: Secara umum, dengan
useState
kamu harus menulis lebih sedikit kode di awal. DenganuseReducer
, kamu harus menulis baik fungsi reducer dan dispatch actions. Namun,useReducer
dapat membantu mengurangi jumlah kode jika banyak event handler memodifikasi state dengan cara yang serupa. - Keterbacaan:
useState
sangat mudah dibaca ketika pembaruan state sederhana. Ketika pembaruan semakin kompleks, mereka dapat membesarkan kode komponen dan sulit untuk dipindai. Dalam hal ini,useReducer
memungkinkan kamu untuk memisahkan dengan jelas bagaimana logika pembaruan dipisahkan dari apa yang terjadi pada event handler. - Debugging: Ketika kamu memiliki bug dengan
useState
, sulit untuk mengetahui di mana state diatur dengan tidak benar, dan mengapa. DenganuseReducer
, kamu dapat menambahkan log konsol ke reducer kamu untuk melihat setiap pembaruan state, dan mengapa itu terjadi (karenaaksi
apa). Jika setiapaksi
benar, kamu akan tahu bahwa kesalahan ada di logika reducer itu sendiri. Namun, kamu harus melalui kode yang lebih banyak daripadauseState
. - Testing: Sebuah reducer adalah fungsi murni yang tidak bergantung pada komponen kamu. Ini berarti kamu dapat mengekspor dan mengujinya secara terpisah secara isolasi. Meskipun secara umum lebih baik untuk menguji komponen dalam lingkungan yang lebih realistis, untuk logika pembaruan state yang kompleks dapat berguna untuk menegaskan bahwa reducer kamu mengembalikan state tertentu untuk state awal dan aksi tertentu.
- Preferensi pribadi: Beberapa orang suka reducers, yang lain tidak. Itu tidak apa-apa. Ini hanya masalah preferensi. Kamu selalu dapat mengonversi antara fungsi
useState
danuseReducer
bolak-balik: mereka setara!
Kami merekomendasikan menggunakan reducer jika kamu sering menghadapi bug karena pembaruan state yang salah dibeberapa komponen, dan ingin memperkenalkan lebih banyak struktur pada kode-nya. Kamu tidak harus menggunakan fungsi reducers untuk semuanya: bebas untuk mencampur dan mencocokkan! Kamu bahkan dapat menggunakan useState
dan useReducer
di komponen yang sama.
Menulis fungsi reduksi dengan baik
Ingatlah dua tips ini saat menulis fungsi reducers:
- Reducer harus murni (pure). Sama seperti fungsi updater state, reducer dijalankan selama proses rendering! (Aksi diantre sampai render selanjutnya.) Ini berarti bahwa reducer harus murni - input yang sama selalu menghasilkan output yang sama. Mereka tidak boleh mengirim permintaan, menjadwalkan waktu tunggu, atau melakukan efek samping (operasi yang memengaruhi hal-hal di luar komponen). Mereka harus memperbarui objek dan senarai tanpa mutasi.
- Setiap aksi menjelaskan satu interaksi pengguna, meskipun itu mengakibatkan beberapa perubahan pada data. Sebagai contoh, jika pengguna menekan “Reset” pada formulir dengan lima field yang dikelola oleh reducer, lebih baik untuk mengirimkan satu aksi
reset_form
daripada lima aksiset_field
terpisah. Jika Anda mencatat setiap aksi dalam fungsi reducer, log tersebut harus cukup jelas bagi Anda untuk merekonstruksi interaksi atau respon apa yang terjadi dalam urutan apa. Ini membantu dalam proses debugging!
Menulis Reducer yang singkat dengan Immer
Sama seperti memperbarui objek dan senarai pada state biasa, Anda dapat menggunakan pustaka Immer untuk membuat reducer lebih ringkas. Di sini, useImmerReducer
memungkinkan Anda memutasi state dengan push
atau arr[i] =
assignment:
import { useImmerReducer } from 'use-immer'; import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; function tasksReducer(draft, action) { switch (action.type) { case 'added': { draft.push({ id: action.id, text: action.text, done: false, }); break; } case 'changed': { const index = draft.findIndex((t) => t.id === action.task.id); draft[index] = action.task; break; } case 'deleted': { return draft.filter((t) => t.id !== action.id); } default: { throw Error('Unknown action: ' + action.type); } } } export default function TaskApp() { const [tasks, dispatch] = useImmerReducer(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>Prague itinerary</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </> ); } let nextId = 3; const initialTasks = [ {id: 0, text: 'Visit Kafka Museum', done: true}, {id: 1, text: 'Watch a puppet show', done: false}, {id: 2, text: 'Lennon Wall pic', done: false}, ];
Fungsi reducers harus bersifat murni, sehingga tidak boleh mengubah state. Tetapi Immer memberikan objek khusus draft
yang aman untuk dimutasi. Di bawah kap mesin, Immer akan membuat salinan dari state Anda dengan perubahan yang dibuat pada draft
. Inilah sebabnya mengapa reducer yang dikelola oleh useImmerReducer
dapat memutasi argumen pertama mereka dan tidak perlu mengembalikan state.
Rekap
- Untuk mengkonversi dari
useState
keuseReducer
:- Kirim aksi dari event handler.
- Tulis fungsi reducer yang mengembalikan state selanjutnya untuk state dan aksi yang diberikan.
- Ganti
useState
denganuseReducer
.
- Reducer membutuhkan Anda untuk menulis sedikit lebih banyak kode, tetapi membantu dengan debugging dan pengujian.
- Fungsi reducer harus murni.
- Setiap aksi menggambarkan satu interaksi pengguna.
- Gunakan Immer jika Anda ingin menulis fungsi reducer dalam gaya yang dapat dimutasi.
Tantangan 1 dari 4: Meneruskan aksi dari event handlers
Meneruskan tindakan dari penangan acara
Saat ini, penangan acara di ContactList.js
dan Chat.js
memiliki komentar // TODO
. Inilah mengapa mengetik ke input tidak berfungsi, dan mengklik tombol tidak mengubah penerima yang dipilih.
Gantikan dua // TODO
ini dengan kode untuk meneruskan
tindakan yang sesuai. Untuk melihat bentuk yang diharapkan dan jenis tindakan, periksa pengurang dalam messengerReducer.js
. Pengurang sudah ditulis sehingga Anda tidak perlu mengubahnya. Anda hanya perlu meneruskan tindakan di ContactList.js
dan Chat.js
.
import { useReducer } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; import { initialState, messengerReducer } from './messengerReducer'; export default function Messenger() { const [state, dispatch] = useReducer(messengerReducer, initialState); const message = state.message; const contact = contacts.find((c) => c.id === state.selectedId); return ( <div> <ContactList contacts={contacts} selectedId={state.selectedId} dispatch={dispatch} /> <Chat key={contact.id} message={message} contact={contact} dispatch={dispatch} /> </div> ); } const contacts = [ {id: 0, name: 'Taylor', email: 'taylor@mail.com'}, {id: 1, name: 'Alice', email: 'alice@mail.com'}, {id: 2, name: 'Bob', email: 'bob@mail.com'}, ];