Redux kullanarak JavaScript'te değişmezlik

Yayınlanan: 2022-03-11

Sürekli büyüyen zengin ve karmaşık JavaScript uygulamaları ekosisteminde, her zamankinden daha fazla yönetilmesi gereken durum var: mevcut kullanıcı, yüklenen gönderilerin listesi, vb.

Olay geçmişine ihtiyaç duyan herhangi bir veri kümesi durumlu olarak kabul edilebilir. Durumu yönetmek zor ve hataya açık olabilir, ancak değiştirilemez verilerle (değiştirilebilir değil) ve belirli destekleyici teknolojilerle (yani bu makalenin amaçları doğrultusunda Redux) çalışmak önemli ölçüde yardımcı olabilir.

Değişmez verilerin kısıtlamaları vardır, yani oluşturulduktan sonra değiştirilemezler, ancak aynı zamanda, özellikle referansa karşı değer eşitliğinde birçok faydası vardır, bu da verileri sık sık karşılaştırmaya dayanan uygulamaları büyük ölçüde hızlandırabilir (bir şeyin güncellenmesi gerekip gerekmediğini kontrol etme) , Örneğin).

Değişmez durumları kullanmak, genellikle çok, çok daha hızlı olan, veriler üzerinde özyinelemeli bir karşılaştırma yapmaya gerek kalmadan, durumun değişip değişmediğini hızlıca söyleyebilen kod yazmamıza olanak tanır.

Bu makale, eylem yaratıcıları, saf işlevler, oluşturulmuş redüktörler, Redux-saga ve Redux Thunk ile saf olmayan eylemler ve son olarak Redux'un React ile kullanımı yoluyla durumu yönetirken Redux'un pratik uygulamalarını ele alacaktır. Bununla birlikte, MobX, Relay ve Flux tabanlı kütüphaneler gibi Redux'a birçok alternatif var.

Neden Redux?

Redux'u MobX, Relay ve diğer Flux tabanlı uygulamaların çoğu gibi diğer durum kapsayıcılarından ayıran temel özellik, Redux'un yalnızca "eylemler" (düz JavaScript nesneleri) aracılığıyla değiştirilebilen tek bir duruma sahip olmasıdır. Redux mağazası. Diğer veri depolarının çoğu, React bileşenlerinde bulunan duruma sahiptir, birden fazla depoya sahip olmanıza ve/veya değiştirilebilir durumu kullanmanıza izin verir.

Bu da, değişmez veriler üzerinde çalışan saf bir işlev olan mağazanın redüktörünün durumu yürütmesine ve potansiyel olarak güncellemesine neden olur. Bu süreç, anlaşılması daha kolay ve daha belirleyici olan tek yönlü veri akışını zorlar.

Redux Akışı.

Redux düşürücüler, değişmez veriler üzerinde çalışan saf fonksiyonlar olduğundan, aynı girdi verildiğinde her zaman aynı çıktıyı üretirler ve bu da test edilmelerini kolaylaştırır. İşte bir redüktör örneği:

 import Immutable from 'seamless-immutable' const initialState = Immutable([]) // create immutable array via seamless-immutable /** * a reducer takes a state (the current state) and an action object (a plain JavaScript object that was dispatched via dispatch(..) and potentially returns a new state. */ function addUserReducer(state = initialState, action) { if (action.type === 'USERS_ADD') { return state.concat(action.payload) } return state // note that a reducer MUST return a value } // somewhere else... store.dispatch({ type: 'USERS_ADD', payload: user }) // dispatch an action that causes the reducer to execute and add the user

Saf işlevlerle uğraşmak, Redux'un genellikle mutatif durumla kolayca yapılmayan birçok kullanım örneğini kolayca desteklemesini sağlar, örneğin:

  • Zamanda yolculuk (Zamanda geriye, bir önceki duruma gitme)
  • Günlüğe kaydetme (Mağazada bir mutasyona neyin neden olduğunu bulmak için her bir eylemi izleyin)
  • Ortak çalışma ortamları (Örneğin, eylemlerin düz JavaScript nesneleri olduğu ve serileştirilebildiği, kablo üzerinden gönderilebildiği ve başka bir makinede yeniden oynatılabildiği GoogleDocs gibi)
  • Kolay hata raporlama (Gönderilen eylemlerin listesini göndermeniz ve tam olarak aynı durumu elde etmek için bunları yeniden oynatmanız yeterlidir)
  • Optimize edilmiş oluşturma (En azından React gibi bir durum işlevi olarak sanal DOM işleyen çerçevelerde: değişmezlik nedeniyle, nesneleri tekrar tekrar karşılaştırmak yerine referansları karşılaştırarak bir şeyin değişip değişmediğini kolayca anlayabilirsiniz)
  • Saf fonksiyonlar kolayca birim test edilebildiğinden redüktörlerinizi kolayca test edin

Eylem Oluşturucular

Redux'un aksiyon yaratıcıları, kodun temiz ve test edilebilir olmasına yardımcı olur. Redux'daki "eylemlerin", gerçekleşmesi gereken bir mutasyonu tanımlayan düz JavaScript nesnelerinden başka bir şey olmadığını unutmayın. Bununla birlikte, aynı nesneleri tekrar tekrar yazmak tekrarlayıcıdır ve hataya açıktır.

Redux'daki bir eylem yaratıcısı, bir mutasyonu açıklayan düz bir JavaScript nesnesi döndüren basit bir yardımcı işlevdir. Bu, tekrar eden kodu azaltmaya yardımcı olur ve tüm eylemlerinizi tek bir yerde tutar:

 export function usersFetched(users) { return { type: 'USERS_FETCHED', payload: users, } } export function usersFetchFailed(err) { return { type: 'USERS_FETCH_FAILED', payload: err, } } // reducer somewhere else... const initialState = Immutable([]) // create immutable array via seamless-immutable /** * a reducer takes a state (the current state) and an action object (a plain JavaScript object that was dispatched via dispatch(..) and potentially returns a new state. */ function usersFetchedReducer(state = initialState, action) { if (action.type === 'USERS_FETCHED') { return Immutable(action.payload) } return state // note that a reducer MUST return a value }

Redux'u Değiştirilemez Kitaplıklarla Kullanma

Redüktörlerin ve eylemlerin doğası, değişmezlik yardımcı kitaplığı olmadan onları test etmeyi kolaylaştırsa da, sizi mutasyona uğrayan nesnelerden koruyan hiçbir şey yoktur, yani tüm redüktörlerinize yönelik testlerin özellikle sağlam olması gerekir.

Sizi korumak için bir kitaplık olmadan karşılaşacağınız aşağıdaki kod örneğini düşünün:

 const initialState = [] function addUserReducer(state = initialState, action) { if (action.type === 'USERS_ADD') { state.push(action.payload) // NOTE: mutating action!! return state } return state // note that a reducer MUST return a value }

Bu kod örneğinde, önceki durum şimdiki durumla aynı olacağından, zaman yolculuğu bozulacaktır, saf bileşenler, duruma referans, veri olmasına rağmen değişmediği için potansiyel olarak güncellenmeyebilir (veya yeniden oluşturulmayabilir). içeriği değişti ve mutasyonları anlamak çok daha zor.

Değişmezlik kitaplığı olmadan Redux'un sağladığı tüm avantajları kaybederiz. Bu nedenle, özellikle birden çok elin koda dokunduğu büyük bir ekipte çalışırken, immutable.js veya kesintisiz-immutable gibi bir değişmezlik yardımcı kitaplığı kullanmanız şiddetle önerilir.

Hangi kütüphaneyi kullanırsanız kullanın Redux aynı şekilde davranacaktır. Kullanım durumunuza en uygun olanı seçebilmeniz için her ikisinin artılarını ve eksilerini karşılaştıralım:

Immutable.js

Immutable.js, Facebook tarafından oluşturulmuş, Haritalar, Listeler, Kümeler ve Diziler gibi veri yapılarını daha işlevsel bir stile sahip bir kitaplıktır. Değişmez kalıcı veri yapılarından oluşan kütüphanesi, farklı durumlar arasında mümkün olan en az miktarda kopyalamayı gerçekleştirir.

Artıları:

  • Yapısal paylaşım
  • Güncellemelerde daha verimli
  • Daha verimli bellek
  • Güncellemeleri yönetmek için bir dizi yardımcı yönteme sahiptir

Eksileri:

  • Mevcut JS kitaplıklarıyla (yani lodash, ramda) sorunsuz çalışmaz
  • Özellikle hidrasyon / dehidrasyon ve işleme sırasında (toJS / fromJS'den) dönüşüm gerektirir

Kesintisiz-değişmez

Kesintisiz-değişmez, ES5'e kadar geriye dönük uyumlu, değişmez veriler için bir kitaplıktır.

Nesnelerdeki mutasyonları devre dışı bırakmak için defineProperty(..) gibi ES5 özellik tanımlama işlevlerine dayanır. Bu nedenle, lodash ve Ramda gibi mevcut kütüphanelerle tamamen uyumludur. Ayrıca, potansiyel olarak önemli bir performans kazancı sağlayan üretim yapılarında devre dışı bırakılabilir.

Artıları:

  • Mevcut JS kitaplıklarıyla (yani lodash, ramda) sorunsuz çalışır
  • Dönüşümü desteklemek için fazladan kod gerekmez
  • Üretim yapılarında kontroller devre dışı bırakılabilir, bu da performansı artırır

Eksileri:

  • Yapısal paylaşım yok - nesneler/diziler sığ kopyalanır, büyük veri kümeleri için daha yavaş olur
  • Hafıza açısından verimli değil

Redux ve Çoklu Redüktörler

Redux'un bir başka kullanışlı özelliği de redüktörleri birlikte oluşturabilme yeteneğidir. Bu, çok daha karmaşık uygulamalar oluşturmanıza olanak tanır ve herhangi bir kayda değer boyuttaki bir uygulamada, kaçınılmaz olarak birden çok durum türüne sahip olursunuz (mevcut kullanıcı, yüklenen gönderilerin listesi, vb). Redux, bu kullanım durumunu doğal olarak combineReducers işlevini sağlayarak destekler (ve teşvik eder):

 import { combineReducers } from 'redux' import currentUserReducer from './currentUserReducer' import postsListReducer from './postsListReducer' export default combineReducers({ currentUser: currentUserReducer, postsList: postsListReducer, })

Yukarıdaki kodla, currentUser'a dayanan bir bileşene ve currentUser dayanan başka bir bileşene sahip postsList . Bu aynı zamanda performansı da artırır, çünkü herhangi bir tek bileşen, yalnızca kendilerini ilgilendiren ağacın dal(lar)ına abone olacaktır.

Redux'da Saf Olmayan Eylemler

Varsayılan olarak, yalnızca düz JavaScript nesnelerini Redux'a gönderebilirsiniz. Ancak ara katman yazılımıyla Redux, geçerli saati alma, ağ isteği gerçekleştirme, diske dosya yazma vb. gibi saf olmayan eylemleri destekleyebilir.

'Ara katman', gönderilen eylemleri engelleyebilen işlevler için kullanılan terimdir. Bir kez ele geçirildiğinde, diğer çerçevelerdeki (Express.js gibi) ara yazılımlara çok benzer şekilde, eylemi dönüştürmek veya eşzamansız bir eylem göndermek gibi şeyler yapabilir.

Çok yaygın iki ara katman yazılımı kitaplığı, Redux Thunk ve Redux-saga'dır. Redux Thunk zorunlu bir tarzda, Redux-destanı ise işlevsel bir tarzda yazılmıştır. İkisini de karşılaştıralım.

Redux Thunk

Redux Thunk, diğer zincirleme işlevleri döndüren işlevler olan thunks kullanarak Redux içindeki saf olmayan eylemleri destekler. Redux-Thunk'u kullanmak için önce Redux Thunk ara yazılımını mağazaya bağlamanız gerekir:

 import { createStore, applyMiddleware } from 'redux' import thunk from 'redux-thunk' const store = createStore( myRootReducer, applyMiddleware(thunk), // here, we apply the thunk middleware to R )

Artık Redux mağazasına bir thunk göndererek saf olmayan eylemler (bir API çağrısı gerçekleştirmek gibi) gerçekleştirebiliriz:

 store.dispatch( dispatch => { return api.fetchUsers() .then(users => dispatch(usersFetched(users)) // usersFetched is a function that returns a plain JavaScript object (Action) .catch(err => dispatch(usersFetchError(err)) // same with usersFetchError } )

Thunks kullanmanın kodunuzu test etmeyi zorlaştırabileceğini ve kod akışı yoluyla akıl yürütmeyi zorlaştırabileceğini unutmamak önemlidir.

Redux-destan

Redux-saga, üreteçler adı verilen bir ES6 (ES2015) özelliği ve işlevsel/saf yardımcılardan oluşan bir kitaplık aracılığıyla saf olmayan eylemleri destekler. Oluşturucularla ilgili en iyi şey, yeniden başlatılıp duraklatılabilmeleri ve API sözleşmeleri onları test etmeyi son derece kolay hale getirmesidir.

Sagas kullanarak önceki thunk yönteminin okunabilirliğini ve test edilebilirliğini nasıl iyileştirebileceğimizi görelim!

İlk olarak, Redux-destan ara katman yazılımını mağazamıza monte edelim:

 import { createStore, applyMiddleware } from 'redux' import createSagaMiddleware from 'redux-saga' import rootReducer from './rootReducer' import rootSaga from './rootSaga' // create the saga middleware const sagaMiddleware = createSagaMiddleware() // mount the middleware to the store const store = createStore( rootReducer, applyMiddleware(sagaMiddleware), ) // run our saga! sagaMiddleware.run(rootSaga)

Çalıştırmaya başlaması için run(..) işlevinin destanla birlikte çağrılması gerektiğini unutmayın.

Şimdi destanımızı oluşturalım:

 import { call, put, takeEvery } from 'redux-saga/effects' // these are saga effects we'll use export function *fetchUsers(action) { try { const users = yield call(api.fetchUsers) yield put(usersFetched(users)) } catch (err) { yield put(usersFetchFailed(err)) } } export default function *rootSaga() { yield takeEvery('USERS_FETCH', fetchUsers) }

Biri kullanıcı listesini ve rootSaga getiren iki üreteç işlevi tanımladık. api.fetchUsers doğrudan çağırmadığımıza, bunun yerine onu bir çağrı nesnesinde verdiğimize dikkat edin. Bunun nedeni, Redux-saga'nın çağrı nesnesini engellemesi ve saf bir ortam yaratmak için içerdiği işlevi yürütmesidir (üreticileriniz söz konusu olduğunda).

rootSaga , bir USERS_FETCH türüyle gönderilen her eylemi alan ve gerçekleştirdiği fetchUsers destanını çağıran takeEvery, adlı bir işleve tek bir çağrı verir. Gördüğümüz gibi, bu Redux için oldukça öngörülebilir bir yan etki modeli oluşturuyor ve bu da test etmeyi kolaylaştırıyor!

Sagaları Test Etme

Jeneratörlerin destanlarımızı test etmeyi nasıl kolaylaştırdığını görelim. Bu bölümde birim testlerimizi yürütmek için mocha'yı ve iddialar için chai'yi kullanacağız.

Destanlar düz JavaScript nesneleri sağladığından ve bir jeneratör içinde çalıştırıldığından, herhangi bir alay olmadan doğru davranışı sergilediklerini kolayca test edebiliriz! call , take , put , vb. öğelerin yalnızca Redux-saga ara yazılımı tarafından yakalanan düz JavaScript nesneleri olduğunu unutmayın.

 import { take, call } from 'redux-saga/effects' import { expect } from 'chai' import { rootSaga, fetchUsers } from '../rootSaga' describe('saga unit test', () => { it('should take every USERS_FETCH action', () => { const gen = rootSaga() // create our generator iterable expect(gen.next().value).to.be.eql(take('USERS_FETCH')) // assert the yield block does have the expected value expect(gen.next().done).to.be.equal(false) // assert that the generator loops infinitely }) it('should fetch the users if successful', () => { const gen = fetchUsers() expect(gen.next().value).to.be.eql(call(api.fetchUsers)) // expect that the call effect was yielded const users = [ user1, user2 ] // some mock response expect(gen.next(users).value).to.be.eql(put(usersFetched(users)) }) it('should fail if API fails', () => { const gen = fetchUsers() expect(gen.next().value).to.be.eql(call(api.fetchUsers)) // expect that the call effect was yielded const err = { message: 'authentication failed' } // some mock error expect(gen.throw(err).value).to.be.eql(put(usersFetchFailed(err)) }) })

React'le Çalışmak

Redux herhangi bir özel tamamlayıcı kitaplığa bağlı olmasa da, React bileşenleri girdi olarak bir durumu alan ve çıktı olarak sanal bir DOM üreten saf işlevler olduğundan React.js ile özellikle iyi çalışır.

React-Redux, ikisini birbirine bağlayan zor işlerin çoğunu ortadan kaldıran React ve Redux için bir yardımcı kitaplıktır. React-Redux'u en etkili şekilde kullanmak için, sunum bileşenleri ve kapsayıcı bileşenleri kavramını gözden geçirelim.

Sunum bileşenleri, yalnızca oluşturulacak aksesuarlara bağlı olarak nesnelerin görsel olarak nasıl görünmesi gerektiğini tanımlar; sevk işlemleri için aksesuarlardan geri aramalar başlatırlar. Elle yazılmıştır, tamamen saftır ve Redux gibi devlet yönetim sistemlerine bağlı değildir.

Konteyner bileşenleri ise şeylerin nasıl çalışması gerektiğini tanımlar, Redux'un farkındadır, mutasyonları gerçekleştirmek için doğrudan Redux eylemleri gönderir ve genellikle React-Redux tarafından üretilir. Genellikle, aksesuarlarını sağlayan bir sunum bileşeni ile eşleştirilirler.

Redux'ta sunum bileşenleri ve kapsayıcı bileşenleri.

Bir sunum bileşeni yazalım ve onu React-Redux aracılığıyla Redux'a bağlayalım:

 const HelloWorld = ({ count, onButtonClicked }) => ( <div> <span>Hello! You've clicked the button {count} times!</span> <button onClick={onButtonClicked}>Click me</button> </div> ) HelloWorld.propTypes = { count: PropTypes.number.isRequired, onButtonClicked: PropTypes.func.isRequired, }

Bunun, işlev görmesi için tamamen donanımlarına dayanan "aptal" bir bileşen olduğunu unutmayın. Bu harika, çünkü React bileşenini test etmeyi ve oluşturmayı kolaylaştırıyor. Şimdi bu bileşeni Redux'a nasıl bağlayacağımıza bakalım, ancak önce Yüksek Dereceli Bileşenin ne olduğunu ele alalım.

Üst Düzey Bileşenler

React-Redux, Redux'un farkında olan "aptal" bir React bileşeninden daha yüksek dereceli bir bileşen oluşturan connect( .. ) adlı bir yardımcı işlev sağlar.

React, bileşenleri diğer bileşenlere sardığınız zaman olan bileşim yoluyla genişletilebilirliği ve yeniden kullanılabilirliği vurgular. Bu bileşenleri sarmak, davranışlarını değiştirebilir veya yeni işlevler ekleyebilir. Bir konteyner bileşeni olan Redux'un farkında olan sunum bileşenimizden nasıl daha yüksek dereceli bir bileşen oluşturabileceğimizi görelim.

İşte bunu nasıl yapacağınız:

 import { connect } from 'react-redux' const mapStateToProps = state => { // state is the state of our store // return the props that we want to use for our component return { count: state.count, } } const mapDispatchToProps = dispatch => { // dispatch is our store dispatch function // return the props that we want to use for our component return { onButtonClicked: () => { dispatch({ type: 'BUTTON_CLICKED' }) }, } } // create our enhancer function const enhancer = connect(mapStateToProps, mapDispatchToProps) // wrap our "dumb" component with the enhancer const HelloWorldContainer = enhancer(HelloWorld) // and finally we export it export default HelloWorldContainer

mapStateToProps ve mapDispatchToProps olmak üzere iki işlev tanımladığımızı unutmayın.

mapStateToProps , Redux durumundan hesaplanan bir nesneyi döndüren saf bir (durum: Nesne) işlevidir. Bu nesne, sarılmış bileşene geçirilen aksesuarlarla birleştirilecektir. Bu aynı zamanda bir seçici olarak da bilinir, çünkü Redux durumunun parçalarını bileşenin props ile birleştirilecek kısımlarını seçer.

mapDispatchToProps da saf bir işlevdir, ancak Redux gönderme işlevinden hesaplanan bir nesneyi döndüren (gönderme: (Eylem) => geçersiz) işlevlerinden biridir. Bu nesne, aynı şekilde, sarılmış bileşene geçirilen aksesuarlar ile birleştirilecektir.

Şimdi konteyner bileşenimizi kullanmak için, konteyner bileşenine hangi mağazanın kullanılacağını söylemek için React-Redux'taki Provider bileşenini kullanmalıyız:

 import { Provider } from 'react-redux' import { render } from 'react-dom' import store from './store' // where ever your Redux store resides import HelloWorld from './HelloWorld' render( ( <Provider store={store}> <HelloWorld /> </Provider> ), document.getElementById('container') )

Provider bileşeni, mağazayı Redux mağazasına abone olan tüm alt bileşenlere yayar, her şeyi tek bir yerde tutar ve hata veya mutasyon noktalarını azaltır!

Redux ile Kod Güveni Oluşturun

Redux'un bu yeni keşfedilen bilgisi, sayısız destekleyici kitaplığı ve React.js ile olan çerçeve bağlantısı ile, durum kontrolü aracılığıyla uygulamanızdaki mutasyonların sayısını kolayca sınırlayabilirsiniz. Güçlü durum denetimi ise daha hızlı hareket etmenize ve daha güvenle sağlam bir kod tabanı oluşturmanıza olanak tanır.