React, Redux ve Immutable.js: Verimli Web Uygulamaları için Malzemeler

Yayınlanan: 2022-03-11

React, Redux ve Immutable.js şu anda en popüler JavaScript kitaplıkları arasındadır ve ön uç geliştirme söz konusu olduğunda hızla geliştiricilerin ilk tercihi haline gelmektedir. Üzerinde çalıştığım birkaç React ve Redux projesinde, React'i kullanmaya başlayan birçok geliştiricinin React'i ve tam potansiyelini kullanmak için nasıl verimli kod yazılacağını tam olarak anlamadığını fark ettim.

Bu Immutable.js eğitiminde, React ve Redux kullanarak basit bir uygulama oluşturacağız ve React'in en yaygın yanlış kullanımlarından bazılarını ve bunlardan kaçınmanın yollarını belirleyeceğiz.

Veri Referansı Sorunu

React tamamen performansla ilgilidir. Yeni veri değişikliklerini karşılamak için yalnızca DOM'nin minimum parçalarını yeniden oluşturarak, son derece performanslı olacak şekilde sıfırdan inşa edilmiştir. Herhangi bir React uygulaması çoğunlukla küçük basit (veya durumsuz işlev) bileşenlerden oluşmalıdır. Akıl yürütmeleri kolaydır ve çoğu, false döndüren mustComponentUpdate işlevine sahip olabilir.

 shouldComponentUpdate(nextProps, nextState) { return false; }

Performans açısından, en önemli bileşen yaşam döngüsü işlevi mustComponentUpdate'dir ve mümkünse her zaman false döndürmelidir. Bu, bu bileşenin hiçbir zaman yeniden oluşturulmamasını (ilk oluşturma dışında) etkili bir şekilde React uygulamasının son derece hızlı hissetmesini sağlar.

Durum böyle olmadığında, amacımız eski donanım/durum ile yeni donanım/durum arasında ucuz bir eşitlik kontrolü yapmak ve veriler değişmezse yeniden oluşturmayı atlamaktır.

Bir saniyeliğine geriye gidelim ve JavaScript'in farklı veri türleri için eşitlik kontrollerini nasıl gerçekleştirdiğini gözden geçirelim.

boolean , string ve integer gibi ilkel veri türleri için eşitlik kontrolü, her zaman gerçek değerleriyle karşılaştırıldıklarından çok basittir:

 1 === 1 'string' === 'string' true === true

Öte yandan, nesneler , diziler ve işlevler gibi karmaşık türler için eşitlik denetimi tamamen farklıdır. Aynı referansa sahiplerse (bellekte aynı nesneye işaret eden) iki nesne aynıdır.

 const obj1 = { prop: 'someValue' }; const obj2 = { prop: 'someValue' }; console.log(obj1 === obj2); // false

obj1 ve obj2 aynı görünse de referansları farklıdır. Farklı olduklarından, onları mustComponentUpdate işlevi içinde saf bir şekilde karşılaştırmak, bileşenimizin gereksiz yere yeniden oluşturulmasına neden olur.

Unutulmaması gereken önemli nokta, Redux redüktörlerinden gelen verilerin doğru kurulmadıkları takdirde her zaman farklı referanslarla sunulacağı ve bu da bileşenin her seferinde yeniden oluşturmasına neden olacağıdır.

Bu, bileşenlerin yeniden oluşturulmasını önleme arayışımızda temel bir sorundur.

Referansları İşleme

Derinlemesine yuvalanmış nesnelere sahip olduğumuz ve onu önceki sürümüyle karşılaştırmak istediğimiz bir örnek alalım. Yinelemeli olarak iç içe nesne destekleri arasında dolaşabilir ve her birini karşılaştırabiliriz, ancak açıkçası bu son derece pahalı olacaktır ve söz konusu olamaz.

Bu bize tek bir çözüm bırakır, o da referansı kontrol etmektir, ancak yeni sorunlar hızla ortaya çıkar:

  • Hiçbir şey değişmediyse referansı korumak
  • Yuvalanmış nesne/dizi destek değerlerinden herhangi biri değiştiyse referansı değiştirme

Güzel, temiz ve performansı optimize edilmiş bir şekilde yapmak istiyorsak, bu kolay bir iş değildir. Facebook bu sorunu uzun zaman önce fark etti ve Immutable.js'yi kurtarmaya çağırdı.

 import { Map } from 'immutable'; // transform object into immutable map let obj1 = Map({ prop: 'someValue' }); const obj2 = obj1; console.log(obj1 === obj2); // true obj1 = obj1.set('prop', 'someValue'); // set same old value console.log(obj1 === obj2); // true | does not break reference because nothing has changed obj1 = obj1.set('prop', 'someNewValue'); // set new value console.log(obj1 === obj2); // false | breaks reference

Immutable.js işlevlerinin hiçbiri, verilen veriler üzerinde doğrudan mutasyon gerçekleştirmez. Bunun yerine veriler dahili olarak klonlanır, mutasyona uğratılır ve herhangi bir değişiklik varsa yeni referans döndürülür. Aksi takdirde ilk referansı döndürür. Yeni başvuru, obj1 = obj1.set(...); gibi açıkça ayarlanmalıdır. .

React, Redux ve Immutable.js Örnekleri

Bu kitaplıkların gücünü göstermenin en iyi yolu basit bir uygulama oluşturmaktır. Ve bir yapılacaklar uygulamasından daha basit ne olabilir?

Kısaca, bu makalede, uygulamanın yalnızca bu kavramlar için kritik olan kısımlarını inceleyeceğiz. Uygulama kodunun tüm kaynak kodu GitHub'da bulunabilir.

Uygulama başlatıldığında, minimum olan DOM yeniden oluşturma miktarını açıkça göstermek için konsol.log çağrılarının uygun şekilde önemli alanlara yerleştirildiğini fark edeceksiniz.

Diğer tüm yapılacaklar uygulamalarında olduğu gibi, yapılacaklar listesinin bir listesini göstermek istiyoruz. Kullanıcı bir yapılacaklar öğesine tıkladığında onu tamamlandı olarak işaretleyeceğiz. Ayrıca, yeni yapılacaklar eklemek için üstte küçük bir giriş alanına ve altta kullanıcının aşağıdakiler arasında geçiş yapmasına izin verecek 3 filtreye ihtiyacımız var:

  • Tüm
  • Tamamlanmış
  • Aktif

Redux Redüktör

Redux uygulamasındaki tüm veriler tek bir mağaza nesnesi içinde yaşar ve redüktörlere mağazayı daha küçük parçalara ayırmanın ve akıl yürütmesi daha kolay olan uygun bir yol olarak bakabiliriz. Redüktör de bir fonksiyon olduğu için daha da küçük parçalara bölünebilir.

Redüktörümüz 2 küçük parçadan oluşacaktır:

  • yapılacaklar listesi
  • aktifFiltre
 // reducers/todos.js import * as types from 'constants/ActionTypes'; // we can look at List/Map as immutable representation of JS Array/Object import { List, Map } from 'immutable'; import { combineReducers } from 'redux'; function todoList(state = List(), action) { // default state is empty List() switch (action.type) { case types.ADD_TODO: return state.push(Map({ // Every switch/case must always return either immutable id: action.id, // or primitive (like in activeFilter) state data text: action.text, // We let Immutable decide if data has changed or not isCompleted: false, })); // other cases... default: return state; } } function activeFilter(state = 'all', action) { switch (action.type) { case types.CHANGE_FILTER: return action.filter; // This is primitive data so there's no need to worry default: return state; } } // combineReducers combines reducers into a single object // it lets us create any number or combination of reducers to fit our case export default combineReducers({ activeFilter, todoList, });

Redux ile bağlantı

Artık Immutable.js verileriyle bir Redux redüktörü kurduğumuza göre, verileri iletmek için React bileşenine bağlayalım.

 // components/App.js import { connect } from 'react-redux'; // ….component code const mapStateToProps = state => ({ activeFilter: state.todos.activeFilter, todoList: state.todos.todoList, }); export default connect(mapStateToProps)(App);

Mükemmel bir dünyada, connect yalnızca en üst düzey rota bileşenlerinde yapılmalı, verileri mapStateToProps'ta çıkarmalı ve gerisi çocuklara temel React iletme sahneleridir. Büyük ölçekli uygulamalarda, tüm bağlantıları takip etmek zorlaşıyor, bu yüzden onları minimumda tutmak istiyoruz.

State.todos öğesinin Redux CombineReducers işlevinden döndürülen normal bir JavaScript nesnesi olduğunu (todos redüktörün adıdır) not etmek çok önemlidir, ancak state.todos.todoList bir Değişmez Listedir ve böyle bir durumda kalması çok önemlidir. gerekirComponentUpdate kontrolünden geçene kadar form.

Bileşenin Yeniden Oluşturulmasını Önleme

Daha derine inmeden önce, bileşene ne tür verilerin sunulması gerektiğini anlamak önemlidir:

  • Her türden ilkel türler
  • Nesne/dizi yalnızca değişmez biçimde

Bu tür verilere sahip olmak, React bileşenlerine gelen donanımları yüzeysel olarak karşılaştırmamızı sağlar.

Sonraki örnek, mümkün olan en basit şekilde aksesuarların nasıl dağıtılacağını gösterir:

 $ npm install react-pure-render
 import shallowEqual from 'react-pure-render/shallowEqual'; shouldComponentUpdate(nextProps, nextState) { return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState); }

SığEqual işlevi, sahne/durum farkını yalnızca 1 düzey derinlikte kontrol eder. Son derece hızlı çalışır ve değişmez verilerimizle mükemmel bir sinerji içindedir. Bu mustComponentUpdate'i her bileşene yazmak çok elverişsiz olurdu, ama neyse ki basit bir çözüm var.

ShouldComponentUpdate'i özel bir ayrı bileşene çıkarın:

 // components/PureComponent.js import React from 'react'; import shallowEqual from 'react-pure-render/shallowEqual'; export default class PureComponent extends React.Component { shouldComponentUpdate(nextProps, nextState) { return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState); } }

Ardından, bu mustComponentUpdate mantığının istendiği herhangi bir bileşeni genişletin:

 // components/Todo.js export default class Todo extends PureComponent { // Component code }

Bu, çoğu durumda bileşenlerin yeniden oluşturulmasını önlemenin çok temiz ve etkili bir yoludur ve daha sonra uygulama daha karmaşık hale gelir ve aniden özel bir çözüm gerektirirse, kolayca değiştirilebilir.

İşlevleri sahne olarak geçirirken PureComponent kullanılırken küçük bir sorun var. React, ES6 class ile bunu fonksiyonlara otomatik olarak bağlamadığı için manuel olarak yapmamız gerekiyor. Aşağıdakilerden birini yaparak bunu başarabiliriz:

  • ES6 ok işlevi bağlamasını kullanın: <Component onClick={() => this.handleClick()} />
  • bağlamayı kullanın: <Component onClick={this.handleClick.bind(this)} />

Her iki yaklaşım da Bileşenin yeniden oluşturulmasına neden olur çünkü onClick'e her seferinde farklı referanslar iletilir.

Bu soruna geçici bir çözüm bulmak için, yapıcı yöntemindeki işlevleri şu şekilde önceden bağlayabiliriz:

 constructor() { super(); this.handleClick = this.handleClick.bind(this); } // Then simply pass the function render() { return <Component onClick={this.handleClick} /> }

Kendinizi çoğu zaman birden çok işlevi önceden bağlarken bulursanız, küçük yardımcı işlevi dışa aktarabilir ve yeniden kullanabiliriz:

 // utils/bind-functions.js export default function bindFunctions(functions) { functions.forEach(f => this[f] = this[f].bind(this)); } // some component constructor() { super(); bindFunctions.call(this, ['handleClick']); // Second argument is array of function names }

Çözümlerin hiçbiri işinize yaramazsa, mustComponentUpdate koşullarını her zaman manuel olarak yazabilirsiniz.

Bir Bileşen İçinde Değişmez Verileri İşleme

Mevcut değişmez veri kurulumuyla, yeniden oluşturmadan kaçınıldı ve bir bileşenin aksesuarlarında değişmez verilerle baş başa kaldık. Bu değişmez verileri kullanmanın birçok yolu vardır, ancak en yaygın hata, değişmez toJS işlevini kullanarak verileri hemen düz JS'ye dönüştürmektir.

Değişmez verileri derinden düz JS'ye dönüştürmek için toJS'yi kullanmak, yeniden oluşturmadan kaçınmanın tüm amacını ortadan kaldırır çünkü beklendiği gibi çok yavaştır ve bundan kaçınılmalıdır. Peki değişmez verileri nasıl ele alacağız?

Olduğu gibi kullanılması gerekir, bu nedenle Immutable API çok çeşitli işlevler sağlar, eşler ve en yaygın olarak React bileşeni içinde kullanılır. Redux Redüktör'den gelen todoList veri yapısı, değişmez formda bir nesneler dizisidir ve her nesne tek bir yapılacaklar öğesini temsil eder:

 [{ id: 1, text: 'todo1', isCompleted: false, }, { id: 2, text: 'todo2', isCompleted: false, }]

Immutable.js API, normal JavaScript'e çok benzer, bu nedenle todoList'i diğer nesne dizileri gibi kullanırdık. Harita işlevi çoğu durumda en iyi sonucu verir.

Bir harita geri çağrısının içinde, hala değişmez formda bir nesne olan todo alırız ve onu Todo bileşenine güvenle iletebiliriz.

 // components/TodoList.js render() { return ( // …. {todoList.map(todo => { return ( <Todo key={todo.get('id')} todo={todo}/> ); })} // …. ); }

Aşağıdakiler gibi değişmez veriler üzerinde birden çok zincirleme yineleme gerçekleştirmeyi planlıyorsanız:

 myMap.filter(somePred).sort(someComp)

… o zaman önce onu toSeq kullanarak Seq'e dönüştürmek ve yinelemelerden sonra aşağıdaki gibi istenen forma döndürmek çok önemlidir:

 myMap.toSeq().filter(somePred).sort(someComp).toOrderedMap()

Immutable.js hiçbir zaman verilen verileri doğrudan mutasyona uğratmadığı için, her zaman bunun başka bir kopyasını oluşturması gerekir, bunun gibi birden çok yineleme yapmak çok pahalı olabilir. Seq tembel değişmez veri dizisidir, yani ara kopyaların oluşturulmasını atlarken görevini yapmak için mümkün olduğunca az işlem gerçekleştirecektir. Seq bu şekilde kullanılmak üzere inşa edildi.

Todo bileşeninin içinde, aksesuarları almak için get veya getIn kullanın.

Yeterince basit değil mi?

Fark ettim ki, çoğu zaman çok sayıda get() ve özellikle getIn() olması çok okunamaz hale gelebilir. Bu yüzden performans ve okunabilirlik arasında bir tatlı nokta bulmaya karar verdim ve bazı basit deneylerden sonra Immutable.js toObject ve toArray işlevlerinin çok iyi çalıştığını öğrendim.

Bu işlevler, sığ bir şekilde (1 seviye derin) Immutable.js nesnelerini/dizilerini düz JavaScript nesnelerine/dizilerine dönüştürür. İçeride derinlemesine yuvalanmış herhangi bir verimiz varsa, bunlar aktarılmaya hazır değişmez biçimde kalacaklardır. bileşenleri çocuklar ve tam olarak ihtiyacımız olan şey bu.

Sadece ihmal edilebilir bir farkla get() işlevinden daha yavaştır, ancak çok daha temiz görünür:

 // components/Todo.js render() { const { id, text, isCompleted } = this.props.todo.toObject(); // ….. }

Hepsini Eylemde Görelim

Kodu henüz GitHub'dan klonlamadıysanız, şimdi bunu yapmanın tam zamanı:

 git clone https://github.com/rogic89/ToDo-react-redux-immutable.git cd ToDo-react-redux-immutable

Sunucuyu başlatmak şu kadar basittir (Node.js ve NPM'nin kurulu olduğundan emin olun):

 npm install npm start 

Immutable.js örneği: Yapılacaklar Uygulaması, bir "yapılacakları girin" alanı, beş yapılacaklar (ikinci ve dördüncü çarpı işaretli), tümü ve tamamlanmış vs. etkin için bir radyo seçicisi ve bir "tümünü sil" düğmesi.

Web tarayıcınızda http://localhost:3000'e gidin. Geliştirici konsolu açıkken, birkaç yapılacaklar öğesi eklerken günlükleri izleyin, bunları tamamlandı olarak işaretleyin ve filtreyi değiştirin:

  • 5 yapılacaklar öğesi ekle
  • Filtreyi "Tümü"nden "Etkin"e ve ardından tekrar "Tümü"ne değiştirin
    • Yeniden oluşturma yok, sadece filtre değişikliği
  • 2 yapılacakları tamamlandı olarak işaretle
    • İki todo yeniden oluşturuldu, ancak her seferinde yalnızca bir tane
  • Filtreyi "Tümü"nden "Etkin"e ve ardından tekrar "Tümü"ne değiştirin
    • Yalnızca 2 tamamlanmış yapılacak öğe eklendi/çıkarıldı
    • Aktif olanlar yeniden işlenmedi
  • Listenin ortasından tek bir yapılacaklar öğesini silin
    • Yalnızca kaldırılan yapılacaklar öğesi etkilendi, diğerleri yeniden oluşturulmadı

Sarmak

Doğru kullanıldığında React, Redux ve Immutable.js sinerjisi, büyük web uygulamalarında sıklıkla karşılaşılan birçok performans sorununa zarif çözümler sunar.

Immutable.js, derin eşitlik kontrollerinin verimsizliklerine başvurmadan JavaScript nesnelerindeki/dizilerindeki değişiklikleri tespit etmemize olanak tanır ve bu da React'in gerekli olmadığında pahalı yeniden oluşturma işlemlerinden kaçınmasını sağlar. Bu, Immutable.js performansının çoğu senaryoda iyi olma eğiliminde olduğu anlamına gelir.

Umarım makaleyi beğenmişsinizdir ve gelecekteki projelerinizde React'in yenilikçi çözümlerini oluşturmak için faydalı bulmuşsunuzdur.

İlgili: React Components, UI Testini Nasıl Kolaylaştırıyor?