Immer ile Daha İyi Redüktörler
Yayınlanan: 2022-03-10Bir React geliştiricisi olarak, durumun doğrudan mutasyona uğramaması gerektiği ilkesine zaten aşina olmalısınız. Bunun ne anlama geldiğini merak ediyor olabilirsiniz (çoğumuz başladığımızda bu kafa karışıklığını yaşadı).
Bu öğretici bunun hakkını verecektir: değişmez durumun ne olduğunu ve buna duyulan ihtiyacı anlayacaksınız. Ayrıca değişmez durumla çalışmak için Immer'ı nasıl kullanacağınızı ve onu kullanmanın faydalarını öğreneceksiniz. Bu makaledeki kodu bu Github deposunda bulabilirsiniz.
JavaScript'te Değişmezlik ve Neden Önemlidir?
Immer.js, belirtilen misyonu "değişmez durumla daha uygun bir şekilde çalışmanıza" izin vermek olan Michel Weststrate tarafından yazılmış küçük bir JavaScript kitaplığıdır.
Ancak Immer'a dalmadan önce, JavaScript'teki değişmezlik ve bunun bir React uygulamasında neden önemli olduğu hakkında hızlı bir bilgi tazeleyelim.
En son ECMAScript (aka JavaScript) standardı, dokuz yerleşik veri türünü tanımlar. Bu dokuz türden altı tanesi primitive
değerler/türler olarak adlandırılır. Bu altı temel undefined
, number
, string
, boolean
, bigint
ve symbol
. JavaScript'in typeof
operatörüyle yapılan basit bir kontrol, bu veri türlerinin türlerini ortaya çıkaracaktır.
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
, nesne olmayan ve yöntemi olmayan bir değerdir. Mevcut tartışmamız için en önemli şey, bir ilkel değerinin bir kez oluşturulduktan sonra değiştirilemeyeceği gerçeğidir. Bu nedenle, ilkellerin immutable
olduğu söylenir.
Kalan üç tür null
, object
ve function
. Ayrıca typeof
operatörünü kullanarak türlerini de kontrol edebiliriz.
console.log(typeof null) // object console.log(typeof [0, 1]) // object console.log(typeof {name: 'name'}) // object const f = () => ({}) console.log(typeof f) // function
Bu türler mutable
. Bu, değerlerinin oluşturulduktan sonra herhangi bir zamanda değiştirilebileceği anlamına gelir.
[0, 1]
dizisinin neden yukarıda olduğunu merak ediyor olabilirsiniz. JavaScriptland'de bir dizi, yalnızca özel bir nesne türüdür. Ayrıca null
hakkında merak ediyorsanız ve bunun undefined
ne kadar farklı olduğunu merak ediyorsanız. undefined
basitçe, bir değişken için bir değer ayarlamadığımız anlamına gelirken, null
nesneler için özel bir durumdur. Bir şeyin nesne olması gerektiğini biliyorsanız ancak nesne orada değilse, basitçe null
değerini döndürürsünüz.
Basit bir örnekle açıklamak için aşağıdaki kodu tarayıcı konsolunuzda çalıştırmayı deneyin.
console.log('aeiou'.match(/[x]/gi)) // null console.log('xyzabc'.match(/[x]/gi)) // [ 'x' ]
String.prototype.match
, bir object
türü olan bir dizi döndürmelidir. Böyle bir nesne bulamadığında null
döndürür. undefined
döndürmek burada da mantıklı olmaz.
Yeter artık. Değişmezliği tartışmaya dönelim.
MDN belgelerine göre:
“Nesneler dışındaki tüm türler değişmez değerler (yani değiştirilemeyen değerler) tanımlar.”
Bu ifade, özel bir JavaScript nesnesi türü oldukları için işlevleri içerir. Buradaki fonksiyon tanımına bakın.
Değişebilir ve değişmez veri türlerinin pratikte ne anlama geldiğine hızlıca bir göz atalım. Aşağıdaki kodu tarayıcı konsolunuzda çalıştırmayı deneyin.
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
Sonuçlarımız, b'nin a
"türetilmiş" olmasına rağmen, b
değerini değiştirmenin b
değerini etkilemediğini a
. Bunun nedeni, JavaScript motorunun b = a
ifadesini çalıştırdığında, yeni, ayrı bir bellek konumu oluşturması, buraya 5
koyması ve bu konumda b
göstermesi gerçeğinden kaynaklanmaktadır.
Peki ya nesneler? Aşağıdaki kodu göz önünde bulundurun.
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"}
name özelliğini d
değişkeni aracılığıyla değiştirmenin, onu c
de değiştirdiğini görebiliriz. Bu, JavaScript motoru c = { name: 'some name
'
}
ifadesini çalıştırdığında, JavaScript motorunun bellekte bir boşluk oluşturması, nesneyi içine koyması ve c
işaret etmesi gerçeğinden kaynaklanır. Ardından, d = c
ifadesini çalıştırdığında, JavaScript motoru yalnızca d
yi aynı konuma işaret eder. Yeni bir bellek konumu oluşturmaz. Bu nedenle, d
deki öğelerde yapılan herhangi bir değişiklik, örtük olarak c
öğeler üzerinde bir işlemdir. Çok fazla çaba harcamadan, bunun neden yapımda sorun olduğunu görebiliriz.
Bir React uygulaması geliştirdiğinizi ve bir yerde c
değişkeninden okuyarak kullanıcının adını some name
ad olarak göstermek istediğinizi hayal edin. Ancak başka bir yerde, d
nesnesini değiştirerek kodunuza bir hata eklemiştiniz. Bu, kullanıcının adının new name
olarak görünmesine neden olur. c
ve d
ilkel olsaydı bu sorunu yaşamazdık. Ancak ilkeller, tipik bir React uygulamasının sürdürmesi gereken durum türleri için çok basittir.
Bu, uygulamanızda değişmez bir durumu korumanın önemli olmasının başlıca nedenleri ile ilgilidir. Immutable.js README'deki bu kısa bölümü okuyarak birkaç diğer hususa göz atmanızı tavsiye ederim: değişmezlik durumu.
Bir React uygulamasında neden değişmezliğe ihtiyacımız olduğunu anladıktan sonra, şimdi Immer'ın produce
işleviyle sorunu nasıl çözdüğüne bir göz atalım.
Immer'ın produce
İşlevi
Immer'ın çekirdek API'si çok küçüktür ve üzerinde çalışacağınız ana işlev produce
işlevidir. produce
yalnızca bir başlangıç durumu ve durumun nasıl mutasyona uğratılması gerektiğini tanımlayan bir geri arama alır. Geri aramanın kendisi, amaçlanan tüm güncellemeleri yaptığı durumun bir taslak (aynı, ancak yine de bir kopyası) kopyasını alır. Son olarak, uygulanan tüm değişikliklerle yeni, değişmez bir durum produce
.
Bu tür bir durum güncellemesi için genel kalıp şudur:
// produce signature produce(state, callback) => nextState
Bunun pratikte nasıl çalıştığını görelim.
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) })
Yukarıdaki kodda, sadece başlangıç durumunu ve mutasyonların nasıl olmasını istediğimizi belirten bir geri aramayı iletiyoruz. Bu kadar basit. Devletin başka bir yerine dokunmamıza gerek yok. initState
dokunulmadan bırakır ve devletin, başlangıç ve yeni durumlar arasında dokunmadığımız kısımlarını yapısal olarak paylaşır. Eyaletimizde böyle bir parça pets
dizisidir. Üretim d produce
, yaptığımız değişikliklerin yanı sıra değiştirmediğimiz nextState
da içeren değişmez bir durum ağacıdır.

Bu basit ama faydalı bilgilerle donanmış olarak, produce
React redüktörlerimizi basitleştirmemize nasıl yardımcı olabileceğine bir göz atalım.
Immer ile Redüktör Yazma
Aşağıda tanımlanan durum nesnesine sahip olduğumuzu varsayalım.
const initState = { pets: ['dog', 'cat'], packages: [ { name: 'react', installed: true }, { name: 'redux', installed: true }, ], };
Ve yeni bir nesne eklemek istedik ve sonraki adımda installed
anahtarını true
olarak ayarladık.
const newPackage = { name: 'immer', installed: false };
Bunu JavaScripts nesnesi ve dizi yayma sözdizimi ile olağan şekilde yapacak olsaydık, durum düşürücümüz aşağıdaki gibi görünebilirdi.
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; } };
Bu nispeten basit durum nesnesi için bunun gereksiz yere ayrıntılı olduğunu ve hatalara eğilimli olduğunu görebiliriz. Biz de devletin gereksiz olan her yerine dokunmak zorundayız. Şimdi bunu Immer ile nasıl basitleştirebileceğimizi görelim.
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; } });
Ve birkaç satır kodla redüktörümüzü büyük ölçüde basitleştirdik. Ayrıca, varsayılan duruma düşersek, Immer hiçbir şey yapmamıza gerek kalmadan taslak durumunu döndürür. Nasıl daha az ortak kod olduğuna ve durum yayılmasının ortadan kaldırıldığına dikkat edin. Immer ile kendimizi yalnızca durumun güncellemek istediğimiz kısmıyla ilgileniyoruz. `UPDATE_INSTALLED` eyleminde olduğu gibi böyle bir öğe bulamazsak, başka hiçbir şeye dokunmadan devam ediyoruz. 'Üretmek' işlevi de köri için uygundur. "Üretmek" için ilk argüman olarak bir geri arama iletmek, körleme için kullanılmak üzere tasarlanmıştır. Körili 'üretim'in imzası //curried produce signature produce(callback) => (state) => nextState
Körili bir ürünle önceki halimizi nasıl güncelleyebileceğimize bakalım. Körili ürünümüz şöyle görünür: 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; } });
Körili üretim işlevi, bir işlevi ilk argümanı olarak kabul eder ve yalnızca şimdi bir sonraki durumu üretmek için bir durum gerektiren körleştirilmiş bir ürün döndürür. Fonksiyonun ilk argümanı taslak durumudur (bu köri ürünü çağırırken geçilecek durumdan türetilecektir). Ardından, işleve iletmek istediğimiz her sayıda argümanı takip eder.
Şimdi bu işlevi kullanmak için tek yapmamız gereken sonraki durumu ve eylem nesnesini bu şekilde üretmek istediğimiz duruma geçmek.
// 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, });
Bir React uygulamasında useReducer
kancasını kullanırken, durumu yukarıda yaptığım gibi açıkça iletmemize gerek olmadığını unutmayın, çünkü bununla ilgilenir.
Merak ediyor olabilirsiniz, bu günlerde React'teki her şey gibi Immer bir hook
alacak mı? İyi haberlerle birliktesiniz. Immer'ın durumla çalışmak için iki kancası vardır: useImmer
ve useImmerReducer
kancaları. Nasıl çalıştıklarını görelim.
useImmer
Ve useImmerReducer
Kancalarını Kullanma
useImmer
kancasının en iyi açıklaması, use-immer README'nin kendisinden gelir.
useImmer(initialState)
,useState
öğesine çok benzer. İşlev bir demet döndürür, demetin ilk değeri mevcut durumdur, ikincisi ise üretici sona erene ve değişiklikler yapılana kadardraft
serbestçe mutasyona uğrayabileceği bir daldırma üretici işlevini kabul eden updater işlevidir. değişmez ve bir sonraki durum haline gelir.
Bu kancalardan yararlanmak için, ana Immer kitaplığına ek olarak bunları ayrı olarak kurmanız gerekir.
yarn add immer use-immer
Kod terimleriyle, useImmer
kancası aşağıdaki gibi görünür
import React from "react"; import { useImmer } from "use-immer"; const initState = {} const [ data, updateData ] = useImmer(initState)
Ve bu kadar basit. Bunun React'in useState olduğunu söyleyebilirsiniz, ancak biraz steroid ile. Güncelleme işlevini kullanmak çok basittir. Taslak halini alır ve aşağıdaki gibi istediğiniz kadar değiştirebilirsiniz.
// make changes to data updateData(draft => { // modify the draft as much as you want. })
Immer'ın yaratıcısı, nasıl çalıştığını görmek için oynayabileceğiniz bir kod ve kutu örneği sağlamıştır.
useImmerReducer
, useReducer
kancasını kullandıysanız benzer şekilde kullanımı kolaydır. Benzer bir imza var. Bakalım kod açısından nasıl görünüyor.
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);
Redüktörün istediğimiz kadar değiştirebileceğimiz bir draft
durumu aldığını görebiliriz. Ayrıca burada denemeniz için bir kod ve kutu örneği de var.
Immer kancalarını kullanmak işte bu kadar basit. Ama yine de projenizde neden Immer'ı kullanmanız gerektiğini merak ediyorsanız, Immer'ı kullanmak için bulduğum en önemli nedenlerin bir özetini burada bulabilirsiniz.
Neden Immer'ı Kullanmalısınız?
Herhangi bir süre için durum yönetimi mantığı yazdıysanız, Immer'ın sunduğu basitliği hemen takdir edeceksiniz. Ancak Immer'ın sunduğu tek avantaj bu değil.
Immer'ı kullandığınızda, nispeten basit redüktörlerde gördüğümüz gibi, daha az ortak kod yazarsınız. Bu aynı zamanda derin güncellemeleri nispeten kolay hale getirir.
Immutable.js gibi kitaplıklarla, değişmezliğin avantajlarından yararlanmak için yeni bir API öğrenmeniz gerekir. Ancak Immer ile aynı şeyi normal JavaScript Objects
, Arrays
, Sets
ve Maps
ile elde edersiniz. Öğrenecek yeni bir şey yok.
Immer ayrıca varsayılan olarak yapısal paylaşım sağlar. Bu basitçe, bir durum nesnesinde değişiklik yaptığınızda, Immer'ın durumun değişmeyen kısımlarını yeni durum ve önceki durum arasında otomatik olarak paylaştığı anlamına gelir.
Immer ile ayrıca otomatik nesne dondurma elde edersiniz, bu da produced
durumda değişiklik yapamayacağınız anlamına gelir. Örneğin, Immer'ı kullanmaya başladığımda, sort
yöntemini Immer'ın üretim işlevi tarafından döndürülen bir dizi nesneye uygulamaya çalıştım. Dizide herhangi bir değişiklik yapamayacağımı söyleyen bir hata verdi. sort
uygulamadan önce dizi dilim yöntemini uygulamam gerekiyordu. Bir kez daha, üretilen nextState
değişmez bir durum ağacıdır.
Immer ayrıca gzip ile sıkıştırıldığında yalnızca 3 KB'de güçlü bir şekilde yazılmıştır ve çok küçüktür.
Çözüm
Durum güncellemelerini yönetmeye gelince, Immer'ı kullanmak benim için hiç de kolay değil. Tamamen yeni bir şey öğrenmeye çalışmadan JavaScript hakkında öğrendiğiniz her şeyi kullanmaya devam etmenizi sağlayan çok hafif bir kitaplık. Projenize kurmanızı ve hemen kullanmaya başlamanızı tavsiye ederim. Ekleyebilir, mevcut projelerde kullanabilir ve redüktörlerinizi kademeli olarak güncelleyebilirsiniz.
Ayrıca Michael Weststrate'ın Immer giriş blog yazısını okumanızı tavsiye ederim. Özellikle ilginç bulduğum kısım “Immer nasıl çalışır?” Immer'ın proxy'ler gibi dil özelliklerinden ve yazma üzerine kopyalama gibi kavramlardan nasıl yararlandığını açıklayan bölüm.
Ayrıca şu blog gönderisine bir göz atmanızı tavsiye ederim: JavaScript'te Değişmezlik: Yazar Steven de Salas'ın değişmezliği sürdürmenin yararları hakkındaki düşüncelerini sunduğu bir Contratian View.
Umarım bu yazıda öğrendiklerinizle Immer'ı hemen kullanmaya başlayabilirsiniz.
alakalı kaynaklar
-
use-immer
, GitHub - Immer, GitHub
-
function
, MDN web belgeleri, Mozilla -
proxy
, MDN web belgeleri, Mozilla - Nesne (bilgisayar bilimi), Wikipedia
- "JS'de Değişmezlik", Orji Chidi Matthew, GitHub
- “ECMAScript Veri Tipleri ve Değerleri,” Ecma International
- JavaScript, Immutable.js , GitHub için değişmez koleksiyonlar
- "Değişmezlik durumu," Immutable.js , GitHub