useReducer
useReducer
adalah React Hook yang memungkinkan Anda menambahkan reducer ke komponen Anda.
const [state, dispatch] = useReducer(reducer, initialArg, init?)
- Reference
- Penggunaan
- Pemecahan masalah
- Saya telah mendispatch suatu action, tetapi catatan log memberi saya nilai state lama
- Saya telah mendispatch sebuah action, tetapi layar tidak diperbarui
- Bagian dari reducer state menjadi undefined setelah dispatching
- Seluruh reducer state saya menjadi undefined setelah dispatching
- Saya mendapat error: “Too many re-renders”
- Fungsi reducer atau inisialisasi saya berjalan dua kali
Reference
useReducer(reducer, initialArg, init?)
Panggil useReducer
di tingkat atas komponen Anda untuk mengelola statenya dengan reducer.
import { useReducer } from 'react';
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...
Lihat contoh lainnya di bawah.
Parameter
reducer
: Fungsi reducer yang menentukan bagaimana state diperbarui. Itu harus murni, harus mengambil state dan action sebagai argumen, dan harus mengembalikan state berikutnya. State dan action bisa dari tipe apa saja.initialArg
: Nilai dari mana initial state dihitung. Bisa menjadi nilai dari tipe apapun. Bagaimana initial state dihitung darinya bergantung pada argumeninit
berikutnya.- opsional
init
: Fungsi penginisialisasi yang harus mengembalikan initial state. Jika tidak ditentukan, initial state disetel keinitialArg
. Jika tidak, initial state disetel ke hasil pemanggilaninit(initialArg)
.
Pengembalian
useReducer
mengembalikan sebuah array dengan dua nilai:
- State saat ini. Selama render pertama, ini disetel ke
init(initialArg)
atauinitialArg
(jika tidak adainit
). - Fungsi
dispatch
yang memungkinkan Anda memperbarui state ke nilai yang berbeda dan memicu render ulang.
Peringatan
useReducer
adalah sebuah Hook, jadi Anda hanya dapat memanggilnya di tingkat atas komponen Anda atau Hook Anda sendiri. Anda tidak dapat memanggilnya di dalam loop atau pengkondisian. Jika Anda perlu melakukannya, ekstrak komponen baru dan pindahkan state ke dalamnya.- Dalam Strict Mode, React akan memanggil reducer dan inisialisasi Anda sebanyak dua kali untuk membantu Anda menemukan ketidakmurnian yang tidak disengaja. Ini adalah perilaku khusus untuk tahap pengembangan dan tidak mempengaruhi tahap produksi. Jika reducer dan inisialisasi Anda murni (sebagai mestinya), ini tidak akan mempengaruhi logika Anda. Hasil dari salah satu panggilan diabaikan.
Fungsi dispatch
Fungsi dispatch
yang dikembalikan oleh useReducer
memungkinkan Anda memperbarui state ke nilai yang berbeda dan memicu render ulang. Anda harus meneruskan action sebagai satu-satunya argumen ke fungsi dispatch
:
const [state, dispatch] = useReducer(reducer, { age: 42 });
function handleClick() {
dispatch({ type: 'incremented_age' });
// ...
React akan mengatur state berikutnya ke hasil pemanggilan fungsi reducer
yang telah Anda sediakan dengan state
saat ini dan action yang telah Anda teruskan ke dispatch
.
Parameter
action
: Tindakan yang dilakukan oleh pengguna. Ini bisa menjadi nilai tipe apapun. Menurut konvensi, suatu action biasanya berupa objek dengan propertitype
yang mengidentifikasinya dan, secara opsional, properti lain dengan informasi tambahan.
Pengembalian
Fungsi dispatch
tidak memiliki nilai pengembalian.
Peringatan
-
Fungsi
dispatch
hanya memperbarui variabel state untuk render berikutnya. Jika Anda membaca variabel state setelah memanggil fungsidispatch
, anda masih akan mendapatkan nilai lama yang ada di layar sebelum panggilan Anda. -
Jika nilai baru yang Anda berikan identik dengan
state
saat ini, sebagaimana ditentukan oleh perbandinganObject.is
, React akan melewati rendering ulang komponen dan childrennya. Ini adalah pengoptimalan. React mungkin masih perlu memanggil komponen Anda sebelum mengabaikan hasilnya, tetapi itu tidak akan memengaruhi kode Anda. -
React mengelompokkan pembaruan state. Itu memperbarui layar setelah semua event handler berjalan dan telah memanggil fungsi
set
mereka. Ini mencegah beberapa render ulang selama event tunggal. Dalam kasus yang jarang terjadi, Anda perlu memaksa React untuk memperbarui layar lebih awal, misalnya untuk mengakses DOM, Anda dapat menggunakanflushSync
.
Penggunaan
Menambahkan reducer ke komponen
Panggil useReducer
di tingkat atas komponen Anda untuk mengelola state dengan reducer.
import { useReducer } from 'react';
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...
useReducer
mengembalikan sebuah array dengan dua item:
- State saat ini dari variabel state, awalnya diatur ke initial state yang Anda berikan.
- Fungsi
dispatch
yang memungkinkan Anda mengubahnya sebagai respon terhadap interaksi.
Untuk memperbarui apa yang ada di layar, panggil dispatch
dengan objek yang mewakili apa yang dilakukan pengguna, yang disebut action:
function handleClick() {
dispatch({ type: 'incremented_age' });
}
React akan meneruskan state saat ini dan action ke fungsi reducer Anda. Reducer Anda akan menghitung dan mengembalikan state berikutnya. React akan menyimpan state berikutnya, merender komponen Anda dengannya, dan memperbarui UI.
import { useReducer } from 'react'; function reducer(state, action) { if (action.type === 'incremented_age') { return { age: state.age + 1 }; } throw Error('Action tidak diketahui.'); } export default function Counter() { const [state, dispatch] = useReducer(reducer, { age: 42 }); return ( <> <button onClick={() => { dispatch({ type: 'incremented_age' }) }}> Tambahkan umur </button> <p>Halo! Anda berumur {state.age}.</p> </> ); }
useReducer
sangat mirip dengan useState
, tetapi memungkinkan Anda memindahkan logika pembaruan state dari event handler ke dalam satu fungsi di luar komponen Anda. Baca selengkapnya tentang memilih antara useState
dan useReducer
.
Menulis fungsi reducer
Fungsi reducer dideklarasikan seperti ini:
function reducer(state, action) {
// ...
}
Lalu Anda perlu mengisi kode yang akan menghitung dan mengembalikan state berikutnya. Menurut konvensi, biasanya ditulis sebagai switch
statement. Untuk setiap case
di switch
, hitung dan kembalikan beberapa state berikutnya.
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
name: state.name,
age: state.age + 1
};
}
case 'changed_name': {
return {
name: action.nextName,
age: state.age
};
}
}
throw Error('Action tidak diketahui: ' + action.type);
}
Action dapat berbentuk apa saja. Menurut konvensi, meneruskan objek dengan properti type
yang mengidentifikasi action adalah hal yang umum. Itu harus mencakup informasi minimal yang diperlukan yang dibutuhkan reducer untuk menghitung state berikutnya.
function Form() {
const [state, dispatch] = useReducer(reducer, { name: 'Taylor', age: 42 });
function handleButtonClick() {
dispatch({ type: 'incremented_age' });
}
function handleInputChange(e) {
dispatch({
type: 'changed_name',
nextName: e.target.value
});
}
// ...
Nama action type bersifat lokal untuk komponen Anda. Setiap action menjelaskan satu interaksi, meskipun hal itu menyebabkan beberapa perubahan pada data. Bentuk dari state bersifat arbitrer, tetapi biasanya berupa objek atau array.
Baca mengekstrak logika state menjadi reducer untuk mempelajari lebih lanjut.
Contoh 1 dari 3: Form (object)
Dalam contoh ini, reducer mengelola objek state dengan dua field: name
dan age
import { useReducer } from 'react'; function reducer(state, action) { switch (action.type) { case 'incremented_age': { return { name: state.name, age: state.age + 1 }; } case 'changed_name': { return { name: action.nextName, age: state.age }; } } throw Error('Action tidak diketahui: ' + action.type); } const initialState = { name: 'Taylor', age: 42 }; export default function Form() { const [state, dispatch] = useReducer(reducer, initialState); function handleButtonClick() { dispatch({ type: 'incremented_age' }); } function handleInputChange(e) { dispatch({ type: 'changed_name', nextName: e.target.value }); } return ( <> <input value={state.name} onChange={handleInputChange} /> <button onClick={handleButtonClick}> Tambahkan umur </button> <p>Halo, {state.nama}. Anda berumur {state.umur}.</p> </> ); }
Menghindari membuat ulang initial state
React menyimpan initial state sekali dan mengabaikannya pada render berikutnya.
function createInitialState(username) {
// ...
}
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, createInitialState(username));
// ...
Meskipun hasil dari createInitialState(username)
hanya digunakan untuk render awal, Anda tetap memanggil fungsi ini pada setiap render. Hal ini dapat menjadi boros jika Anda membuat array yang besar atau melakukan perhitungan yang rumit.
Untuk mengatasi hal ini, Anda dapat meneruskannya sebagai fungsi initializer ke useReducer
sebagai argumen ketiga:
function createInitialState(username) {
// ...
}
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, username, createInitialState);
// ...
Perhatikan bahwa Anda meneruskan createInitialState
yang merupakan fungsi itu sendiri, dan bukan createInitialState()
, yang merupakan hasil dari pemanggilan fungsi tersebut. Dengan cara ini, initial state tidak dibuat ulang setelah inisialisasi.
Pada contoh di atas, createInitialState
mengambil argumen username
. Jika inisialisasi Anda tidak memerlukan informasi apa pun untuk menghitung initial state, Anda bisa meneruskan null
sebagai argumen kedua ke useReducer
.
Contoh 1 dari 2: Meneruskan fungsi inisialisasi
Contoh ini meneruskan fungsi inisialisasi, sehingga fungsi createInitialState
hanya berjalan selama inisialisasi. Fungsi ini tidak berjalan ketika komponen dirender ulang, seperti ketika Anda mengetikkan input.
import { useReducer } from 'react'; function createInitialState(username) { const initialTodos = []; for (let i = 0; i < 50; i++) { initialTodos.push({ id: i, text: username + "'s task #" + (i + 1) }); } return { draft: '', todos: initialTodos, }; } function reducer(state, action) { switch (action.type) { case 'changed_draft': { return { draft: action.nextDraft, todos: state.todos, }; }; case 'added_todo': { return { draft: '', todos: [{ id: state.todos.length, text: state.draft }, ...state.todos] } } } throw Error('Action tidak diketahui: ' + action.type); } export default function TodoList({ username }) { const [state, dispatch] = useReducer( reducer, username, createInitialState ); return ( <> <input value={state.draft} onChange={e => { dispatch({ type: 'changed_draft', nextDraft: e.target.value }) }} /> <button onClick={() => { dispatch({ type: 'added_todo' }); }}>Tambah</button> <ul> {state.todos.map(item => ( <li key={item.id}> {item.text} </li> ))} </ul> </> ); }
Pemecahan masalah
Saya telah mendispatch suatu action, tetapi catatan log memberi saya nilai state lama
Memanggil fungsi dispatch
tidak mengubah state dalam kode yang sedang berjalan:
function handleClick() {
console.log(state.umur); // 42
dispatch({ type: 'incremented_age' }); // Request render ulang dengan 43
console.log(state.umur); // Masih 42!
setTimeout(() => {
console.log(state.umur); // Juga masih 42!
}, 5000);
}
Hal ini karena state berperilaku seperti snapshot. Memperbarui state akan merequest render ulang dengan nilai state yang baru, tetapi tidak memengaruhi variabel JavaScript state
di dalam event handler yang sudah berjalan.
Jika Anda perlu menebak nilai state berikutnya, Anda dapat menghitungnya secara manual dengan memanggil reducer sendiri:
const action = { type: 'incremented_age' };
dispatch(action);
const nextState = reducer(state, action);
console.log(state); // { age: 42 }
console.log(nextState); // { age: 43 }
Saya telah mendispatch sebuah action, tetapi layar tidak diperbarui
React akan mengabaikan pembaruan Anda jika state berikutnya sama dengan state sebelumnya, seperti yang ditentukan oleh perbandingan Object.is
. Hal ini biasanya terjadi ketika Anda mengubah sebuah objek atau sebuah array pada state secara langsung:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// 🚩 Salah: mengubah objek yang sudah ada
state.age++;
return state;
}
case 'changed_name': {
// 🚩 Salah: mengubah objek yang sudah ada
state.name = action.nextName;
return state;
}
// ...
}
}
Anda mengubah objek state
yang sudah ada dan mengembalikannya, sehingga React mengabaikan pembaruan tersebut. Untuk memperbaikinya, Anda harus memastikan bahwa Anda selalu memperbarui objek dalam state dan memperbarui array dalam state, bukannya memutasi objek tersebut:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ✅ Benar: membuat objek baru
return {
...state,
age: state.age + 1
};
}
case 'changed_name': {
// ✅ Benar: membuat objek baru
return {
...state,
name: action.nextName
};
}
// ...
}
}
Bagian dari reducer state menjadi undefined setelah dispatching
Pastikan bahwa setiap cabang case
menyalin semua field yang ada saat mengembalikan state yang baru:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
...state, // Jangan lupakan ini!
age: state.age + 1
};
}
// ...
Tanpa ...state
di atas, state berikutnya yang dikembalikan hanya akan berisi field age
dan tidak ada yang lain.
Seluruh reducer state saya menjadi undefined setelah dispatching
Jika state Anda secara tidak terduga menjadi undefined
, kemungkinan Anda lupa untuk return
state di salah satu case, atau action type Anda tidak sesuai dengan pernyataan case
mana pun. Untuk mengetahui penyebabnya, buat Throw error di luar switch
:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ...
}
case 'edited_name': {
// ...
}
}
throw Error('Action tidak diketahui: ' + action.type);
}
Anda juga dapat menggunakan pemeriksa tipe statis seperti TypeScript untuk menangkap error tersebut.
Saya mendapat error: “Too many re-renders”
Anda mungkin akan mendapatkan pesan error yang berbunyi: Too many re-renders. React limits the number of renders to prevent an infinite loop.
Biasanya, ini berarti Anda mendispatch sebuah action tanpa syarat selama render, sehingga komponen Anda masuk ke dalam perulangan: render, dispatch (yang menyebabkan render), dan seterusnya. Sering kali, hal ini disebabkan oleh kesalahan dalam menentukan event handler:
// 🚩 Salah: memanggil handler selama render
return <button onClick={handleClick()}>Klik aku</button>
// ✅ Benar: meneruskan event handler
return <button onClick={handleClick}>Klik aku</button>
// ✅ Benar: meneruskan fungsi sebaris
return <button onClick={(e) => handleClick(e)}>Klik aku</button>
Jika Anda tidak dapat menemukan penyebab error ini, klik tanda panah di samping error di konsol dan lihat tumpukan JavaScript untuk menemukan pemanggilan fungsi dispatch
yang bertanggungjawab atas error tersebut.
Fungsi reducer atau inisialisasi saya berjalan dua kali
Pada Strict Mode, React akan memanggil fungsi reducer dan inisialisasi dua kali. Hal ini seharusnya tidak akan merusak kode Anda.
Perilaku development-only ini membantu Anda menjaga komponen tetap murni. React menggunakan hasil dari salah satu pemanggilan, dan mengabaikan hasil dari pemanggilan lainnya. Selama fungsi komponen, inisialisasi, dan reducer Anda murni, hal ini tidak akan mempengaruhi logika Anda. Namun, jika mereka tidak murni secara tidak sengaja, hal ini akan membantu Anda untuk mengetahui kesalahannya.
Sebagai contoh, fungsi reducer yang tidak murni ini mengubah sebuah array dalam state:
function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// 🚩 Kesalahan: mengubah state
state.todos.push({ id: nextId++, text: action.text });
return state;
}
// ...
}
}
Karena React memanggil fungsi reducer dua kali, Anda akan melihat todo ditambahkan dua kali, sehingga Anda akan tahu bahwa ada kesalahan. Pada contoh ini, Anda dapat memperbaiki kesalahan dengan mengganti array alih-alih melakukan mutasi:
function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// ✅ Benar: mengganti dengan state baru
return {
...state,
todos: [
...state.todos,
{ id: nextId++, text: action.text }
]
};
}
// ...
}
}
Karena fungsi reducer ini murni, maka memanggilnya sebagai waktu tambahan tidak akan membuat perbedaan pada perilakunya. Inilah sebabnya mengapa React memanggilnya dua kali akan membantu Anda menemukan kesalahan. Hanya fungsi komponen, inisialisasi, dan reducer yang harus murni. Event handler tidak perlu murni, sehingga React tidak akan pernah memanggil event handler dua kali.
Baca menjaga komponen tetap murni untuk mempelajari lebih lanjut.