useReducer adalah React Hook yang memungkinkan Anda menambahkan reducer ke komponen Anda.

const [state, dispatch] = useReducer(reducer, initialArg, init?)

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 argumen init berikutnya.
  • opsional init: Fungsi penginisialisasi yang harus mengembalikan initial state. Jika tidak ditentukan, initial state disetel ke initialArg. Jika tidak, initial state disetel ke hasil pemanggilan init(initialArg).

Pengembalian

useReducer mengembalikan sebuah array dengan dua nilai:

  1. State saat ini. Selama render pertama, ini disetel ke init(initialArg) atau initialArg (jika tidak ada init).
  2. 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 properti type 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 fungsi dispatch, 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 perbandingan Object.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 menggunakan flushSync.


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:

  1. State saat ini dari variabel state, awalnya diatur ke initial state yang Anda berikan.
  2. 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.

Sandungan

State bersifat read-only. Jangan ubah objek atau array apapun dalam state:

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// 🚩 Jangan memutasikan objek dalam state seperti ini:
state.age = state.age + 1;
return state;
}

Sebagai gantinya, selalu kembalikan objek baru dari reducer Anda:

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ✅ Sebagai gantinya, kembalikan objek baru
return {
...state,
age: state.age + 1
};
}

Baca memperbarui objek dalam state dan memperbarui array dalam state untuk mempelajari lebih lanjut.

Contoh penggunaan dasar useReducer

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.

Perbedaan antara meneruskan inisialisasi dan meneruskan initial state secara langsung

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.