useReducer adalah hook React yang menyimpan state seperti useState, tapi memindahkan setiap cara mengubahnya ke satu fungsi, yaitu reducer. Komponenmu memanggil dispatch dengan object action seperti { type: 'increment' }, lalu React memberikan state saat ini dan action itu ke reducer, yang mengembalikan state berikutnya.
Tombol-tombolnya tidak menyebutkan bagaimana angkanya berubah, hanya apa yang terjadi. Semua aritmetikanya ada di reducer. Tambahkan case 'double' yang mengembalikan { count: state.count * 2 } dan sebuah tombol yang men-dispatch-nya untuk melihat bagaimana jenis pembaruan baru masuk ke dalamnya.
Sintaksnya
const [state, dispatch] = useReducer(reducer, initialArg, init); // init is optional
function reducer(state, action) {
// return the next state
}
reduceradalah fungsi(state, action) => nextState. Definisikan di luar komponen; fungsi ini tidak butuh apa pun dari scope komponen.initialArgadalah state awal, hanya dipakai di render pertama.initbersifat opsional. Kalau kamu memberikannya, state awalnya menjadiinit(initialArg).stateadalah state saat ini untuk render ini.dispatch(action)mengirim action ke reducer dan menjadwalkan render dengan hasilnya. Fungsi ini stabil: fungsi yang sama di setiap render, jadi aman untuk dikirim ke bawah atau dicantumkan sebagai dependensi.
Action bisa berupa nilai apa pun, tapi menurut konvensi berupa object dengan string type yang menjelaskan apa yang terjadi, ditambah data apa pun yang dibutuhkan reducer: { type: 'added', text: 'Buy milk' }.
Menulis reducer
Sebagian besar reducer berupa switch pada action.type, dengan satu case untuk setiap jenis event. Setiap case mengembalikan state yang benar-benar baru; case tidak pernah mengubah state lama. Case default melempar error, jadi salah ketik seperti dispatch({ type: 'incremnet' }) langsung gagal dengan jelas alih-alih diam saja.
Beri nama action sesuai apa yang dilakukan pengguna (added, toggled, deleted), bukan sesuai perubahan state yang kamu bayangkan (setTodos). Dengan begitu komponen terbaca seperti daftar event, dan reducer menjadi satu-satunya tempat yang menentukan arti setiap event.
Reducer harus murni
Reducer adalah fungsi murni: dengan state dan action yang sama, reducer mengembalikan hasil yang sama, dan tidak melakukan hal lain. Artinya tidak ada mutasi, tidak ada request, tidak ada timer, tidak ada Math.random() atau Date.now() di dalamnya. React bergantung pada hal ini. Di development di bawah StrictMode, React memanggil reducer-mu dua kali untuk setiap action dan menyimpan satu hasilnya, untuk membantumu menyadari reducer yang tidak murni. Pratinjau di sini berjalan seperti build production, jadi contoh-contoh di halaman ini memanggilnya sekali.
Aturan mutasi adalah yang paling sering dilanggar. Melakukan push ke state.todos lalu mengembalikan state berarti mengembalikan object yang sama, jadi React tidak melihat perubahan dan melewati render, jebakan yang sama seperti yang dijelaskan di memperbarui array dan object. Buat array dan object baru dengan spread, map, dan filter.
// Wrong: mutates and returns the same object
case 'added':
state.todos.push(action.todo);
return state;
// Right: returns a new object with a new array
case 'added':
return { ...state, todos: [...state.todos, action.todo] };
Efek samping yang menjadi bagian dari sebuah event, seperti menyimpan ke server, tempatnya di event handler di samping dispatch. Efek samping yang menjadi bagian dari state, seperti menyinkronkannya ke localStorage, tempatnya di effect.
Todo list dengan useReducer
Berikut reducer yang mengerjakan tugas sungguhan: menambah, mencentang, dan menghapus todo. console.log di awal reducer mencetak setiap action, cara yang praktis untuk melihat bagaimana state berubah selagi kamu melakukan debug. Ini satu-satunya efek samping yang umumnya ditoleransi di reducer, karena tidak mengubah apa pun; hapus sebelum dirilis.
Tambahkan sebuah todo, centang satu kotak, dan hapus satu, lalu baca console: setiap perubahan adalah satu baris yang menyebutkan action dan datanya. Teks input tetap disimpan di useState, karena teks itu hanya milik field tersebut dan tidak ada event lain yang menyentuhnya. Mencampur kedua hook di satu komponen itu hal yang biasa.
Id todo baru dibuat di click handler dan dibawa di dalam action, jadi reducer hanya menyalinnya. Menulis id: nextId++ di dalam reducer akan membuatnya tidak murni: di bawah StrictMode dalam development, pemanggilan kedua akan melompati satu id.
Dispatch tidak langsung mengubah state
dispatch bekerja seperti setter useState: fungsi ini menjadwalkan render, dan variabel state di handler saat ini tetap menyimpan nilai lama. Untuk langsung memakai state baru, hitung sendiri dengan memanggil reducer.
Klik beberapa kali. Baris log pertama selalu tertinggal satu langkah, dan baris kedua cocok dengan angka di tombol setelah render. Karena reducer adalah fungsi biasa, memanggilnya sendiri itu aman, satu lagi keuntungan dari menjaganya tetap murni.
useState vs useReducer
Kedua hook menyimpan state, dan apa pun yang kamu tulis dengan salah satunya bisa kamu tulis dengan yang lain. Perbedaannya ada di tempat logika pembaruannya berada.
| useState | useReducer | |
|---|---|---|
| Logika pembaruan | Di setiap event handler | Di satu fungsi reducer |
| Cocok untuk | Beberapa nilai yang berdiri sendiri | Beberapa field yang diubah bersama oleh banyak event |
| Ukuran kode | Lebih sedikit untuk state sederhana | Lebih banyak di awal, lebih sedikit saat event bertambah |
| Pengujian | Uji lewat komponen | Uji reducer sebagai fungsi biasa |
| Debugging | Cari handler mana yang mengaturnya | Catat setiap action di satu tempat |
Pakai useReducer saat kamu melihat state yang sama diperbarui di banyak handler, saat satu event harus mengubah beberapa bagian state yang harus tetap konsisten, atau saat sebuah handler sebagian besar berisi logika tentang state berikutnya. Untuk toggle atau field teks, useState lebih singkat dan lebih jelas. Berpindah belakangan juga tidak sulit: ganti setter dengan dispatch dan pindahkan logikanya ke dalam case.
Inisialisasi lazy
Kalau kamu menulis state awal sebagai pemanggilan fungsi, seperti useReducer(reducer, createInitialState('Ada')), pemanggilan itu berjalan di setiap render, meskipun React hanya memakai hasilnya untuk pertama kali. Kalau membuat state awal itu berat, atau harus diturunkan dari sebuah prop, berikan argumen ketiga: fungsi init. React memanggil init(initialArg) sekali, di render pertama.
Ketik di input: komponen dirender di setiap ketukan tombol, tapi createInitialState hanya mencatat log sekali. Sekarang ganti pemanggilannya menjadi useReducer(reducer, createInitialState('Ada')) lalu ketik lagi. Fungsi itu berjalan di setiap render, dan React membuang hasilnya setiap kali setelah render pertama.
Berikan fungsinya sendiri, bukan hasil pemanggilannya. useReducer(reducer, 'Ada', createInitialState) bersifat lazy; useReducer(reducer, createInitialState('Ada')) tidak.
useReducer dengan context
Reducer cocok dipasangkan dengan context saat pohon yang dalam membutuhkan state-nya. Taruh state dan dispatch di context di bagian atas, dan komponen mana pun di bawahnya bisa membaca state atau mengirim action tanpa props yang diteruskan melewati setiap lapisan. Karena dispatch tidak pernah berubah, komponen yang hanya mengirim action bisa membaca context dispatch yang terpisah dan tidak ikut dirender ulang saat state berubah.
import { createContext, useContext, useReducer } from 'react';
const TodosContext = createContext(null);
const TodosDispatchContext = createContext(null);
export function TodosProvider({ children }) {
const [todos, dispatch] = useReducer(todosReducer, []);
return (
<TodosContext value={todos}>
<TodosDispatchContext value={dispatch}>{children}</TodosDispatchContext>
</TodosContext>
);
}
function AddTodo() {
const dispatch = useContext(TodosDispatchContext);
return <button onClick={() => dispatch({ type: 'added', text: 'New' })}>Add</button>;
}
Di React 19, object context berfungsi sebagai provider-nya sendiri (<TodosContext value={...}>); <TodosContext.Provider> juga masih berfungsi. Halaman useContext membahas bagaimana context sampai ke komponen dan kapan context merender ulang komponen itu.
Pertanyaan yang sering diajukan
Apa itu useReducer di React?
useReducer adalah hook untuk state yang pembaruannya dijelaskan sebagai action. Kamu memanggil const [state, dispatch] = useReducer(reducer, initialState), lalu dispatch({ type: 'added' }) dari event handler. React memberikan state saat ini dan action itu ke reducer milikmu, dan apa pun yang dikembalikannya menjadi state berikutnya.
Kapan sebaiknya memakai useReducer alih-alih useState?
Saat beberapa event memperbarui state yang sama dengan cara yang saling berkaitan, saat state berikutnya bergantung pada beberapa field sekaligus, atau saat logika pembaruannya cukup panjang sehingga kamu ingin menaruhnya di satu fungsi yang bisa diuji di luar komponen. Untuk satu atau dua nilai yang berdiri sendiri, useState lebih sederhana.
Mengapa reducer harus murni?
React mungkin memanggil reducer-mu lebih dari sekali untuk action yang sama (StrictMode sengaja melakukannya di development) dan mengharapkan hasil yang sama setiap kali. Jadi reducer tidak boleh memutasi state, mengambil data, memasang timer, atau membaca nilai acak. Lakukan hal-hal itu di event handler atau effect.
Apakah dispatch langsung memperbarui state?
Tidak. Seperti setter useState, dispatch menjadwalkan render. Di dalam event handler saat ini, state masih berisi nilai lama; state baru muncul di render berikutnya.
Apa argumen ketiga dari useReducer?
Fungsi init yang opsional. Saat kamu memberikannya, React menghitung state awal sebagai init(initialArg), dan hanya di render pertama. Ini berguna saat membuat state awal itu berat atau bergantung pada sebuah prop.