Menu

Memperbarui Array dan Object di State React (Immutable)

Untuk mengubah array atau object di state React, buat yang baru lalu berikan ke setter. Pelajari cara menambah, menghapus, mengganti, menyisipkan, dan mengurutkan item, memperbarui object bersarang, dan mengapa mutasi state membuat layar tidak berubah.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

Di React, kamu memperbarui array atau object di state dengan membuat yang baru lalu memberikannya ke setter. Jangan pernah mengubah nilai yang sudah ada di tempat: pakai spread (...), map, filter, atau slice untuk membuat salinan berisi perubahanmu, lalu panggil setItems(copy).

[...items, { ... }] adalah array baru dengan satu item tambahan, dan filter mengembalikan array baru tanpa item yang dihapus. Keduanya tidak menyentuh array lama.

Mengapa mutasi tidak memicu render

Saat kamu memanggil setter, React membandingkan nilai baru dengan nilai lama memakai Object.is. Untuk array dan object, yang dibandingkan adalah referensinya, bukan isinya. Kalau kamu mengubah array dengan push lalu mengirim array yang sama kembali, React melihat referensi yang sama, menyimpulkan tidak ada yang berubah, dan melewati render.

Klik "push" beberapa kali. Console menunjukkan array-nya bertambah, tapi tidak ada baris render baru dan teksnya tetap a. Lalu klik "spread": satu render terjadi dan semua item yang di-push muncul sekaligus, karena item-item itu sebenarnya sudah ada di array sejak tadi. Mutasi tidak melempar error, mutasi hanya membuat layar ketinggalan sampai ada hal lain yang memicu render. Klik "push" sekali lagi setelah itu dan kali ini mungkin terjadi render: React tidak selalu bisa melewati pekerjaannya sejak awal, jadi mutasi kadang terlihat dan kadang tidak. Ketidakpastian itulah alasan untuk tidak memutasi state.

Array: method yang dipakai dan yang dihindari

Aturannya sederhana: pakai method yang mengembalikan array baru, hindari method yang mengubah array tempat method itu dipanggil.

TujuanHindari (memutasi)Pakai (mengembalikan array baru)
Menambahpush, unshift[...arr, item], [item, ...arr]
Menghapuspop, shift, splicefilter, slice
Menggantiarr[i] = x, splicemap, with(i, x)
Menyisipkansplice[...arr.slice(0, i), x, ...arr.slice(i)], toSpliced
Mengurutkan, membaliksort, reversetoSorted, toReversed, atau salin dulu

toSorted, toReversed, toSpliced, dan with adalah method array yang lebih baru (ES2023), didukung di semua browser utama saat ini sejak 2023. Method ini melakukan pekerjaan yang sama dengan kerabatnya yang memutasi, tapi pada salinan.

Memperbarui satu item dengan map

Untuk mengubah satu item, lakukan map pada array lalu kembalikan object baru untuk item yang kamu inginkan, dan object asli untuk semua item lain.

Bagian { ...todo, done: !todo.done } itu penting. Bug yang umum adalah menyalin array tapi mengubah object di dalamnya:

const next = [...todos];
next[0].done = true; // next is new, but next[0] is the same object as todos[0]
setTodos(next);

Kode ini memicu render, jadi terlihat baik-baik saja, tapi array lama dan array baru sekarang berbagi object yang sudah diubah. Apa pun yang membandingkan item lama dengan item baru (child yang memakai React.memo, effect yang bergantung pada item itu) tidak melihat perbedaan. Salin item yang kamu ubah.

Menyisipkan di indeks tertentu

Tidak ada insert yang tidak memutasi, jadi potong array menjadi dua dan taruh item baru di antara kedua bagian. toSpliced(index, 0, item) melakukan hal yang sama dalam satu pemanggilan.

Atur indeks ke 0 lalu sisipkan lagi: C yang baru masuk ke depan. Ganti ekspresi next dengan letters.toSpliced(index, 0, 'C') dan hasilnya sama.

Mengurutkan dan membalik

sort dan reverse mengubah array tempat keduanya dipanggil, meskipun keduanya juga mengembalikan array itu. Itu membuat setItems(items.sort()) menjadi mutasi yang diikuti set dengan referensi yang sama, yaitu bug dari awal halaman ini. Urutkan salinannya saja.

[...scores].reverse() berhasil karena spread membuat salinannya lebih dulu, lalu reverse hanya mengubah salinan itu.

Memperbarui object dan object bersarang

Object mengikuti aturan yang sama. Sebarkan object lama ke object baru dan timpa field yang berubah. Untuk object bersarang, lakukan spread di setiap level dalam perjalanan turun ke field itu: satu spread hanya menyalin level teratas, jadi object di dalamnya masih object yang lama.

Setiap level yang kamu salin adalah object baru; setiap level yang tidak kamu sentuh tetap object yang sama. Itulah yang membuat spread murah: kalau state juga menyimpan daftar pesanan yang panjang, { ...user } akan menyalin referensi ke daftar itu, bukan daftarnya.

Kalau object di state bersarang tiga atau empat level dan kamu terus menulis rangkaian spread yang panjang, itu sering menjadi tanda state-nya bisa dibuat lebih datar. Menyimpan item dalam lookup berdasarkan id, atau memecah satu object besar menjadi beberapa pemanggilan useState, menjaga setiap pembaruan tetap singkat. Saat banyak event mengubah satu struktur, reducer juga membantu (lihat useReducer).

structuredClone vs spread

structuredClone(user) membuat salinan mendalam (deep copy) dalam satu pemanggilan, jadi setelah itu kamu bebas mengubah salinannya:

const next = structuredClone(user);
next.address.city = 'Oslo';
setUser(next);

Cara ini benar, tapi menyalin seluruh object setiap kali, termasuk bagian yang tidak kamu ubah. Itu memakan waktu pada state yang besar, dan berarti setiap object bersarang mendapat referensi baru, sehingga child yang di-memoize dan menerima user.orders dirender lagi meskipun pesanannya tidak berubah. structuredClone juga melempar error pada fungsi, dan instance class kembali sebagai object biasa. Spread hanya menyalin jalur yang kamu ubah, jadi spread menjadi pilihan yang biasa; simpan structuredClone untuk pembaruan langka yang memang menulis ulang sebagian besar object.

Immer adalah library kecil yang membuatmu bisa menulis kode yang terlihat seperti mutasi. Immer memberimu sebuah draft, mencatat apa yang kamu ubah, lalu menghasilkan nilai immutable baru dengan hanya bagian yang berubah yang disalin. Package use-immer membungkusnya dalam sebuah hook:

import { useImmer } from 'use-immer';

function Profile() {
    const [user, updateUser] = useImmer({
        name: 'Ada',
        address: { city: 'London' },
    });

    function moveTo(city) {
        updateUser((draft) => {
            draft.address.city = city;
        });
    }
    // ...
}

Immer tidak tersedia di editor halaman ini, yang hanya memuat React. Immer layak ditambahkan saat state-mu sangat bersarang dan rangkaian spread mulai sulit dibaca. Untuk state yang datar, spread biasa dan map sudah cukup, dan itulah yang akan kamu lihat di sebagian besar kode React.

Pertanyaan yang sering diajukan

Bagaimana cara menambahkan item ke array di state React?

Buat array baru yang berisi item lama ditambah item baru, lalu berikan ke setter: setItems([...items, newItem]). Jangan memanggil items.push(newItem), karena itu mengubah array yang sudah ada dan React melihat referensi yang sama.

Mengapa komponen saya tidak dirender ulang saat saya memperbarui array?

Kemungkinan besar kamu mengubah array di tempat (push, splice, sort, atau arr[i] = x) lalu mengirim array yang sama kembali. React membandingkan nilai lama dan baru dengan Object.is, menemukan object yang sama, dan melewati render. Berikan array baru sebagai gantinya.

Bagaimana cara memperbarui satu object di dalam array di React?

Pakai map dan kembalikan object baru untuk item yang berubah, serta item aslinya untuk semua item lain: setTodos(todos.map(t => t.id === id ? { ...t, done: !t.done } : t)).

Bagaimana cara memperbarui object bersarang di state React?

Salin setiap level di jalur menuju field yang kamu ubah: setUser({ ...user, address: { ...user.address, city: 'Oslo' } }). Satu spread hanya menyalin level teratas, jadi object di dalamnya masih dipakai bersama.

Sebaiknya pakai structuredClone atau spread operator untuk state React?

Utamakan spread. Spread hanya menyalin bagian yang berubah dan mempertahankan bagian yang tidak disentuh sebagai object yang sama, yang membantu React.memo dan perbandingan. structuredClone menyalin semuanya setiap kali, lebih mudah ditulis tapi lebih lambat pada state yang besar dan merusak kesamaan referensi untuk bagian yang tidak berubah.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai