Reducer Lebih Baik Dengan Immer
Diterbitkan: 2022-03-10Sebagai pengembang React, Anda seharusnya sudah terbiasa dengan prinsip bahwa status tidak boleh dimutasi secara langsung. Anda mungkin bertanya-tanya apa artinya itu (kebanyakan dari kita mengalami kebingungan saat memulai).
Tutorial ini akan melakukan keadilan untuk itu: Anda akan memahami apa itu keadaan yang tidak dapat diubah dan kebutuhannya. Anda juga akan mempelajari cara menggunakan Immer untuk bekerja dengan status yang tidak dapat diubah dan manfaat menggunakannya. Anda dapat menemukan kodenya di artikel ini di repo Github ini.
Kekekalan Dalam JavaScript Dan Mengapa Itu Penting
Immer.js adalah perpustakaan JavaScript kecil yang ditulis oleh Michel Weststrate yang menyatakan misinya adalah untuk memungkinkan Anda "bekerja dengan keadaan yang tidak dapat diubah dengan cara yang lebih nyaman."
Tetapi sebelum menyelami Immer, mari kita segera menyegarkan kembali tentang kekekalan dalam JavaScript dan mengapa itu penting dalam aplikasi React.
Standar ECMAScript (alias JavaScript) terbaru mendefinisikan sembilan tipe data bawaan. Dari sembilan tipe tersebut, ada enam yang disebut sebagai nilai/tipe primitive
. Keenam primitif ini adalah undefined
, number
, string
, boolean
, bigint
, dan symbol
. Pemeriksaan sederhana dengan operator typeof
JavaScript akan mengungkapkan tipe tipe data ini.
console.log(typeof 5) // number console.log(typeof 'name') // string console.log(typeof (1 < 2)) // boolean console.log(typeof undefined) // undefined console.log(typeof Symbol('js')) // symbol console.log(typeof BigInt(900719925474)) // bigint
primitive
adalah nilai yang bukan objek dan tidak memiliki metode. Yang paling penting untuk diskusi kita saat ini adalah fakta bahwa nilai primitif tidak dapat diubah setelah dibuat. Dengan demikian, primitif dikatakan tidak dapat immutable
.
Tiga jenis sisanya adalah null
, object
, dan function
. Kami juga dapat memeriksa jenisnya menggunakan operator typeof
.
console.log(typeof null) // object console.log(typeof [0, 1]) // object console.log(typeof {name: 'name'}) // object const f = () => ({}) console.log(typeof f) // function
Jenis ini mutable
. Ini berarti bahwa nilainya dapat diubah kapan saja setelah dibuat.
Anda mungkin bertanya-tanya mengapa saya memiliki array [0, 1]
di atas sana. Nah, di JavaScriptland, array hanyalah tipe objek khusus. Jika Anda juga bertanya-tanya tentang null
dan apa bedanya dengan undefined
. undefined
berarti bahwa kita belum menetapkan nilai untuk variabel sementara null
adalah kasus khusus untuk objek. Jika Anda tahu sesuatu harus menjadi objek tetapi objek itu tidak ada, Anda cukup mengembalikan null
.
Untuk mengilustrasikannya dengan contoh sederhana, coba jalankan kode di bawah ini di konsol browser Anda.
console.log('aeiou'.match(/[x]/gi)) // null console.log('xyzabc'.match(/[x]/gi)) // [ 'x' ]
String.prototype.match
harus mengembalikan array, yang merupakan tipe object
. Ketika tidak dapat menemukan objek seperti itu, ia mengembalikan null
. Mengembalikan undefined
juga tidak masuk akal di sini.
Cukup dengan itu. Mari kembali membahas kekekalan.
Menurut dokumen MDN:
"Semua jenis kecuali objek mendefinisikan nilai yang tidak dapat diubah (yaitu, nilai yang tidak dapat diubah)."
Pernyataan ini mencakup fungsi karena mereka adalah tipe khusus dari objek JavaScript. Lihat definisi fungsi di sini.
Mari kita lihat sekilas apa yang dimaksud dengan tipe data yang dapat berubah dan tidak dapat diubah dalam praktiknya. Coba jalankan kode di bawah ini di konsol browser Anda.
let a = 5; let b = a console.log(`a: ${a}; b: ${b}`) // a: 5; b: 5 b = 7 console.log(`a: ${a}; b: ${b}`) // a: 5; b: 7
Hasil kami menunjukkan bahwa meskipun b
"diturunkan" dari a
, mengubah nilai b
tidak mempengaruhi nilai a
. Ini muncul dari fakta bahwa ketika mesin JavaScript mengeksekusi pernyataan b = a
, itu menciptakan lokasi memori baru yang terpisah, menempatkan 5
di sana, dan menunjuk b
di lokasi itu.
Bagaimana dengan objek? Pertimbangkan kode di bawah ini.
let c = { name: 'some name'} let d = c; console.log(`c: ${JSON.stringify(c)}; d: ${JSON.stringify(d)}`) // {"name":"some name"}; d: {"name":"some name"} d.name = 'new name' console.log(`c: ${JSON.stringify(c)}; d: ${JSON.stringify(d)}`) // {"name":"new name"}; d: {"name":"new name"}
Kita dapat melihat bahwa mengubah properti nama melalui variabel d
juga mengubahnya di c
. Ini muncul dari fakta bahwa ketika mesin JavaScript mengeksekusi pernyataan, c = { name: 'some name
'
}
, mesin JavaScript menciptakan ruang di memori, menempatkan objek di dalamnya, dan menunjuk c
ke sana. Kemudian, ketika mengeksekusi pernyataan d = c
, mesin JavaScript hanya mengarahkan d
ke lokasi yang sama. Itu tidak membuat lokasi memori baru. Jadi setiap perubahan pada item di d
secara implisit merupakan operasi pada item di c
. Tanpa banyak usaha, kita dapat melihat mengapa ini adalah masalah dalam pembuatannya.
Bayangkan Anda sedang mengembangkan aplikasi React dan di suatu tempat Anda ingin menunjukkan nama pengguna sebagai some name
dengan membaca dari variabel c
. Tetapi di tempat lain Anda telah memperkenalkan bug dalam kode Anda dengan memanipulasi objek d
. Ini akan mengakibatkan nama pengguna muncul sebagai new name
. Jika c
dan d
adalah primitif, kita tidak akan memiliki masalah itu. Tetapi primitif terlalu sederhana untuk jenis status yang harus dipertahankan oleh aplikasi React biasa.
Ini tentang alasan utama mengapa penting untuk mempertahankan status yang tidak dapat diubah dalam aplikasi Anda. Saya mendorong Anda untuk memeriksa beberapa pertimbangan lain dengan membaca bagian singkat ini dari README Immutable.js: kasus kekekalan.
Setelah memahami mengapa kita membutuhkan kekekalan dalam aplikasi React, sekarang mari kita lihat bagaimana Immer menangani masalah dengan fungsi produce
.
Fungsi produce
Immer
API inti Immer sangat kecil, dan fungsi utama yang akan Anda gunakan adalah fungsi produce
. produce
hanya mengambil keadaan awal dan panggilan balik yang mendefinisikan bagaimana keadaan harus bermutasi. Panggilan balik itu sendiri menerima salinan draf (identik, tetapi masih merupakan salinan) dari status yang membuat semua pembaruan yang dimaksud. Akhirnya, ini produce
keadaan baru yang tidak dapat diubah dengan semua perubahan yang diterapkan.
Pola umum untuk pembaruan status semacam ini adalah:
// produce signature produce(state, callback) => nextState
Mari kita lihat bagaimana ini bekerja dalam praktik.
import produce from 'immer' const initState = { pets: ['dog', 'cat'], packages: [ { name: 'react', installed: true }, { name: 'redux', installed: true }, ], } // to add a new package const newPackage = { name: 'immer', installed: false } const nextState = produce(initState, draft => { draft.packages.push(newPackage) })
Dalam kode di atas, kita cukup meneruskan status awal dan panggilan balik yang menentukan bagaimana kita ingin mutasi terjadi. Ini sesederhana itu. Kita tidak perlu menyentuh bagian lain dari negara bagian. Ini membuat initState
tidak tersentuh dan secara struktural berbagi bagian-bagian dari state yang tidak kita sentuh antara state awal dan state baru. Salah satu bagian tersebut di negara kita adalah array pets
. nextState
produce
pohon status yang tidak dapat diubah yang memiliki perubahan yang telah kami buat serta bagian yang tidak kami modifikasi.
Berbekal pengetahuan yang sederhana namun bermanfaat ini, mari kita lihat bagaimana produce
dapat membantu kita menyederhanakan reduksi React kita.
Menulis Reducer Dengan Immer
Misalkan kita memiliki objek status yang didefinisikan di bawah ini
const initState = { pets: ['dog', 'cat'], packages: [ { name: 'react', installed: true }, { name: 'redux', installed: true }, ], };
Dan kami ingin menambahkan objek baru, dan pada langkah berikutnya, atur kunci yang installed
ke true
const newPackage = { name: 'immer', installed: false };
Jika kita melakukan ini dengan cara biasa dengan objek JavaScript dan sintaks penyebaran array, peredam status kita mungkin terlihat seperti di bawah ini.
const updateReducer = (state = initState, action) => { switch (action.type) { case 'ADD_PACKAGE': return { ...state, packages: [...state.packages, action.package], }; case 'UPDATE_INSTALLED': return { ...state, packages: state.packages.map(pack => pack.name === action.name ? { ...pack, installed: action.installed } : pack ), }; default: return state; } };
Kita dapat melihat bahwa ini tidak perlu bertele-tele dan rentan terhadap kesalahan untuk objek keadaan yang relatif sederhana ini. Kita juga harus menyentuh setiap bagian negara bagian, yang sebenarnya tidak perlu. Mari kita lihat bagaimana kita bisa menyederhanakan ini dengan Immer.
const updateReducerWithProduce = (state = initState, action) => produce(state, draft => { switch (action.type) { case 'ADD_PACKAGE': draft.packages.push(action.package); break; case 'UPDATE_INSTALLED': { const package = draft.packages.filter(p => p.name === action.name)[0]; if (package) package.installed = action.installed; break; } default: break; } });
Dan dengan beberapa baris kode, kami telah sangat menyederhanakan peredam kami. Juga, jika kita jatuh ke dalam kasus default, Immer hanya mengembalikan status draf tanpa kita perlu melakukan apa pun. Perhatikan bagaimana ada lebih sedikit kode boilerplate dan penghapusan penyebaran status. Dengan Immer, kami hanya memperhatikan bagian dari status yang ingin kami perbarui. Jika kami tidak dapat menemukan item seperti itu, seperti dalam tindakan `UPDATE_INSTALLED`, kami hanya melanjutkan tanpa menyentuh apa pun. Fungsi `produce` juga cocok untuk kari. Meneruskan callback sebagai argumen pertama ke `produce` dimaksudkan untuk digunakan untuk currying. Tanda tangan dari `produk` kari adalah //curried produce signature produce(callback) => (state) => nextState
Mari kita lihat bagaimana kita dapat memperbarui status kita sebelumnya dengan produk kari. Produk kari kami akan terlihat seperti ini: const curriedProduce = produce((draft, action) => { switch (action.type) { case 'ADD_PACKAGE': draft.packages.push(action.package); break; case 'SET_INSTALLED': { const package = draft.packages.filter(p => p.name === action.name)[0]; if (package) package.installed = action.installed; break; } default: break; } });
Fungsi hasil kari menerima fungsi sebagai argumen pertamanya dan mengembalikan hasil kari yang hanya sekarang memerlukan status untuk menghasilkan status berikutnya. Argumen pertama dari fungsi tersebut adalah status draf (yang akan diturunkan dari status yang akan diteruskan saat memanggil produk kari ini). Kemudian ikuti setiap jumlah argumen yang ingin kita berikan ke fungsi.
Yang perlu kita lakukan sekarang untuk menggunakan fungsi ini adalah meneruskan keadaan dari mana kita ingin menghasilkan keadaan berikutnya dan objek tindakan seperti itu.
// add a new package to the starting state const nextState = curriedProduce(initState, { type: 'ADD_PACKAGE', package: newPackage, }); // update an item in the recently produced state const nextState2 = curriedProduce(nextState, { type: 'SET_INSTALLED', name: 'immer', installed: true, });
Perhatikan bahwa dalam aplikasi Bereaksi saat menggunakan kait useReducer
, kita tidak perlu meneruskan status secara eksplisit seperti yang telah saya lakukan di atas karena itu menanganinya.
Anda mungkin bertanya-tanya, apakah Immer akan mendapatkan hook
, seperti semua yang ada di React akhir-akhir ini? Nah, Anda berada di perusahaan dengan kabar baik. Immer memiliki dua kait untuk bekerja dengan status: kait useImmer
dan useImmerReducer
. Mari kita lihat bagaimana mereka bekerja.
Menggunakan Kait useImmer
Dan useImmerReducer
Deskripsi terbaik dari kait useImmer
berasal dari README use-immer itu sendiri.
useImmer(initialState)
sangat mirip denganuseState
. Fungsi mengembalikan tuple, nilai pertama tuple adalah status saat ini, yang kedua adalah fungsi updater, yang menerima fungsi produser immer, di manadraft
dapat dimutasi secara bebas, hingga produser berakhir dan perubahan akan dilakukan tidak berubah dan menjadi negara berikutnya.
Untuk menggunakan pengait ini, Anda harus memasangnya secara terpisah, di samping perpustakaan Immer utama.
yarn add immer use-immer
Dalam istilah kode, kait useImmer
terlihat seperti di bawah ini
import React from "react"; import { useImmer } from "use-immer"; const initState = {} const [ data, updateData ] = useImmer(initState)
Dan itu sesederhana itu. Anda bisa mengatakan itu React's useState tetapi dengan sedikit steroid. Untuk menggunakan fungsi pembaruan sangat sederhana. Ini menerima status draf dan Anda dapat memodifikasinya sebanyak yang Anda inginkan seperti di bawah ini.
// make changes to data updateData(draft => { // modify the draft as much as you want. })
Pembuat Immer telah memberikan contoh kode dan kotak yang dapat Anda mainkan untuk melihat cara kerjanya.
useImmerReducer
juga mudah digunakan jika Anda telah menggunakan hook useReducer
React. Ini memiliki tanda tangan yang serupa. Mari kita lihat seperti apa itu dalam istilah kode.
import React from "react"; import { useImmerReducer } from "use-immer"; const initState = {} const reducer = (draft, action) => { switch(action.type) { default: break; } } const [data, dataDispatch] = useImmerReducer(reducer, initState);
Kita dapat melihat bahwa peredam menerima status draft
yang dapat kita modifikasi sebanyak yang kita inginkan. Ada juga contoh kode dan kotak di sini untuk Anda coba.
Dan begitulah sederhananya menggunakan kait Immer. Tetapi jika Anda masih bertanya-tanya mengapa Anda harus menggunakan Immer dalam proyek Anda, berikut adalah ringkasan dari beberapa alasan terpenting yang saya temukan untuk menggunakan Immer.
Mengapa Anda Harus Menggunakan Immer
Jika Anda telah menulis logika manajemen status untuk waktu yang lama, Anda akan segera menghargai kesederhanaan yang ditawarkan Immer. Tapi itu bukan satu-satunya manfaat yang ditawarkan Immer.
Saat Anda menggunakan Immer, Anda akhirnya menulis lebih sedikit kode boilerplate seperti yang telah kita lihat dengan reduksi yang relatif sederhana. Ini juga membuat pembaruan mendalam relatif mudah.
Dengan library seperti Immutable.js, Anda harus mempelajari API baru untuk mendapatkan manfaat dari kekekalan. Tapi dengan Immer Anda mencapai hal yang sama dengan JavaScript Objects
, Arrays
, Sets
, dan Maps
yang normal. Tidak ada yang baru untuk dipelajari.
Immer juga menyediakan berbagi struktural secara default. Ini berarti bahwa ketika Anda membuat perubahan pada objek status, Immer secara otomatis membagikan bagian status yang tidak berubah antara status baru dan status sebelumnya.
Dengan Immer, Anda juga mendapatkan pembekuan objek otomatis yang berarti Anda tidak dapat membuat perubahan pada status yang produced
. Misalnya, ketika saya mulai menggunakan Immer, saya mencoba menerapkan metode sort
pada array objek yang dikembalikan oleh fungsi produksi Immer. Itu menimbulkan kesalahan yang memberi tahu saya bahwa saya tidak dapat membuat perubahan apa pun pada array. Saya harus menerapkan metode irisan array sebelum menerapkan sort
. Sekali lagi, nextState
yang dihasilkan adalah state tree yang tidak dapat diubah.
Immer juga diketik dengan kuat dan sangat kecil hanya 3KB saat di-gzip.
Kesimpulan
Dalam hal mengelola pembaruan status, menggunakan Immer bukanlah hal yang sulit bagi saya. Ini adalah pustaka yang sangat ringan yang memungkinkan Anda tetap menggunakan semua hal yang telah Anda pelajari tentang JavaScript tanpa mencoba mempelajari sesuatu yang sama sekali baru. Saya mendorong Anda untuk menginstalnya di proyek Anda dan mulai menggunakannya segera. Anda dapat menambahkan menggunakannya dalam proyek yang ada dan secara bertahap memperbarui reduksi Anda.
Saya juga mendorong Anda untuk membaca posting blog pengantar Immer oleh Michael Weststrate. Bagian yang menurut saya sangat menarik adalah "Bagaimana cara kerja Immer?" bagian yang menjelaskan bagaimana Immer memanfaatkan fitur bahasa seperti proxy dan konsep seperti copy-on-write.
Saya juga mendorong Anda untuk melihat posting blog ini: Kekekalan dalam JavaScript: Pandangan Kontratian di mana penulisnya, Steven de Salas, menyajikan pemikirannya tentang manfaat mengejar kekekalan.
Saya harap dengan hal-hal yang telah Anda pelajari dalam posting ini, Anda dapat segera mulai menggunakan Immer.
Sumber Daya Terkait
-
use-immer
, GitHub - Immer, GitHub
-
function
, dokumen web MDN, Mozilla -
proxy
, dokumen web MDN, Mozilla - Objek (ilmu komputer), Wikipedia
- “Kekekalan dalam JS,” Orji Chidi Matthew, GitHub
- “Tipe dan Nilai Data ECMAScript,” Ecma International
- Koleksi abadi untuk JavaScript, Immutable.js, GitHub
- “Kasus untuk Kekekalan,” Immutable.js , GitHub