Redux'a giriş - 2 - React uygulamasına Redux eklemek

Önceki yazılardan birinde Vanilla JS (bildiğimiz eski JS) ile basit Redux uygulaması hazırlamıştım.

http://cuneyt.aliustaoglu.biz/tr/reduxa-giris-basit-fakat-komple-bir-ornek/

Fakat bu örnek, bir projede kullanılabilecek bir yapıda değil kesinlikle. Ancak, Redux mantığını kavramamda bana oldukça yardımcı olmuştu. Herkes, Redux geliştiricisinin Egghead'deki eğitim videolarının ne kadar faydalı olduğundan bahsetse de tek bir state kullanan bu TODO uygulaması bendeki soru işaretlerinin yalnızca bir kaçını giderebildi. Elbette mantığı tam oturtabilmek için her zamanki gibi sıfırdan kendim geliştirmem gerekecekti.

React ve Redux genelde birlikte anılırlar ancak birbirinden tamamen bağımsız iki kütüphanedir. jQuery kullanarak da pekala React uygulamaları oluşturabilirsiniz. Fakat Redux'ın ve React'in çıkış amacı zaten bileşenlerimizi içeren yapıyı, işlemlerimizi içeren yapıdan ayırmak ve uygulamanın tek veri kaynağı (single source of truth) olmak. jQuery'de bu yapılar birbirine girdiği için ve jQuery direk DOM üzerinde işlem yaptığı için değişkenimizin o anki durumunu elde etmek pek mümkün olmuyor. Neyse bunlar derin konular. Kısaca jQuery ve Redux birlikteliği mümkün olsa da büyük projelerde uzun vadede önerilmiyor.

Ayrıca Redux ve React birbirlerini çok güzel bir şekilde tamamlayan iki kütüphanedir ve başka kütüphanelerle genişletilmeye de oldukça açık bir yapıdalar. Bu yazıda sadelik açısından router, redux-thunk gibi yapıları eklemeyeceğiz. Fakat, bunları veya alternatiflerini orta ölçekli projelerin hepsinde ister istemez kullanmak zorunda kalacağız.

Aslında sıfırdan, yalnızca React uygulaması yaratmak da öyle çok kolay olan bir şey değil. Webpack, babel vs. gibi pek çok yapıyı bir araya getirmek ve debug edilebilir bir ortam yaratmak için yığınla ayar yapmak gerekiyor. Ancak sağolsunlar ki React'i yaratan Facebook mühendisleri bu tip işleri otomatik hale getiren bir konsol uygulaması geliştirmişler.

create-react-app

Eğer create react app uygulaması yüklü değilse npm ile global olarak yükleyelim. Ayrıca redux'ı ve react-redux'ı da ekleyelim. react-redux, redux'ı react bileşenleri ile çalıştırabilmek için gereken fonksiyonları sağlıyor bize.

npm install -g create-react-app

create-react-app redux-uygulamasi

cd redux-uygulamasi/

npm install react-redux --save

npm install redux --save

npm start

Şimdi uygulamamıza bir kaç bileşen ekleyelim. Amacımız genişletilebilir bir React/Redux uygulaması yaratmak olduğu için mümkün olduğunca modüler bir yapı oluşturmaya gayret edelim. Çünkü daha sonra router, redux-thunk veya saga gibi pek çok bileşen ekelemeye çalışacağız. Ancak bugün yalnızca redux üzerine yoğunlaşalım. Aşağıdaki dizin/dosya yapısını kuralım. Şimdilik boş dosyaları oluşturalım daha sonra içlerini dolduracağız.

/Components/
           /Ogrenciler.js
           /Siniflar.js
/redux
           /rootReducer.js
           /store.js
           /reducers/
                    /ogrenciReducer.js
                    /sinifReducer.js

Şimdi yukarıdan aşağıya doğru gidelim. index.js dosyamızı aşağıdaki gibi düzenleyelim. react-redux'ın Provider kütüphanesi tüm React uygulamasında kullanılacak olan store'u uygulamaya bildiriyor. Tek bir tane store, ancak store'un küçük küçük alt parçaları olacak. Bunlara reducer deniyor. Dosyayı kaydettikten sonra web sayfası hata verecektir. Çünkü diğer kısımları henüz bağlamadık.

index.js

App.js dosyasını da aşağıdaki gibi düzenleyelim. App içerisinde Öğrenci ve Sınıf olmak üzere 2 tane bileşen olacak. Genişletilebilir bir yapı örneği açısından bu iki bileşen, birbirinden bağımsız iki reducer kullanacak. 2 reducer ile 100 reducer arasında teknik anlamda hiç fark yok. Tüm reducer'lar rootReducer üzerinden store'a bağlanacak. Bu bağlanma işlemini redux'ın özel fonksiyonları(combineReducers, createStore vs.) halledecek. Uygulamamız genişledikçe ilgili reducer'ları yaratacak ve rootReducer'a referansları ekleyeceğiz. Redux geliştiricisinin ünlü TODO örneği tek bir reducer kullandığından, yeni katıldığım projedeki karmaşık yapıyı anlamama pek yardımcı olmamıştı. O yüzden 1 ile 2 arasındaki fark 2 ile 100 arasındaki farktan oldukça fazla bana göre.

App.js

Şimdi de index.js'te referans verdiğimiz store'u yaratalım. Çok büyük bir dosya değil. Şu an yalnızca rootReducer'ı store'a bağlamakla görevli. Ancak ileride middleware eklememiz gerektiği zaman bu dosyayı güncelleyeceğiz.

redux/store.js

RootReducer'u ekleyelim. Uygulama geliştikçe ekleyeceğimiz dosyaların referanslarını bu dosyada güncelleyeceğiz. Şimdilik sadece öğrenci ve sınıf reducer'larımız var.

redux/rootReducer.js

Geldik reducer'ları eklemeye. Birbirinin benzeri Öğrenci ve Sınıf reducer'ları. Reducer'ların en temel görevi action'lara cevap vermek. Action'ların olmazsa olmaz özelliği type'tır. Eğer dispatch ettiğiniz action, type özelliğine sahip değilse Error: Actions may not have an undefined "type" property. Have you misspelled a constant? hatası alırsınız. "type" dışında çoğu zaman payload gibi(ismi herhangi bir şey olabilir) bir parametre ile de reducer'a göndermek istediğiniz veriyi tutabilirsiniz. Örneğin aşağıdaki Öğrenci reducer'ında, metin kutusu her değiştiğinde OGRENCI.DEGISTIR_AD tipinde bir action dağıtılıyor yani dispatch ediliyor. payload'da ise metin kutusunun değeri gönderiliyor. Şimdi neden state.ogrenciReducer.ogrenciAdi = txtOgrenci.value şeklinde bir ifade kullanmadık da uzun yoldan aslında aynı işi yapan bir eylem yarattık? Redux'ın tüm çıkış amacı yukarıda da bahsettiğim gibi ön yüz uygulamanın bir nevi veritabanı olabilmek. Zaten değer atamasını direk state üzerinden yapmaya kalkışırsanız react/redux buna izin vermeyecektir.

redux/reducers/ogrenciReducer.js

Yukarıdaki kodu biraz daha iredeleyelim. "state" genel anlamda tüm Redux store aslında. Ancak reducerlar'ın içinde iken Redux, yalnızca ilgili state'in parçasını gönderiyor. Bu kısım çok önemli. Aslında benim için Redux'ın çözülme noktası bunu anlayabilmek olmuştu. Bundan sonra Redux mantığı çorap söküğü gibi geldi. combineReducers fonksiyonuyla Redux'a daha önceden reducer'ları göndermiştik. Bu sayede Redux, state'in hangi parçasını reducer'a göndereceğini biliyor. Ayrıca reducer içerisindeki değişkenlerin ilk hali Redux'a bildirilmeli. Çünkü store oluşturulurken bu fonksiyonlar otomatik olarak çağrılıyor. Fakat herhangi bir atama yapılmadığı için Redux hangi değişkenleri oluşturmak zorunda olduğunu bilmiyor. Zorunluluk hali dışında, aslında reducer'ın şablonunu da dosyaya baktığımda ister istemez görmüş oluyorum. Eğer özel bir durumumuz yoksa metin değişkenler için '', nümerik değişkenler için 0 ya da -1, nesneler için {} vs. atamaları yapmak gayet yerinde olacaktır.

Bir başka önemli nokta da Object.assign() kısmı. Burada state'in ilgili parçasını (reducer) al ve ilgili parçanın da yalnızca ilgili kısmını değiştir, tüm parçayı Redux'a güncellemesi için geri gönder. Object.assign({}, state, {sinifAd: "Yeni Sınıf"} yerine Object.assign(state, {sinifAd: "Yeni Sınıf"} kullanırsak state'i değişmeye zorlamış oluyoruz ve bu çok istenen bir durum değil. state'i biz kesinlikle direk olarak değiştirmek istemiyoruz. Redux'a ne yapmak istediğimizi bildiren bir eylem (action) bildirimi yapıyoruz (dispatch). Redux kalan kısmı kendisi hallediyor. Redux bir anlamda ORM(örneğin N-Hibernate) yazılımlarına benziyor aslında. Direk SQL komutu yazmak yerine, aynı işi yapan üst seviye katmanı tetikliyoruz.

Reducer ve state

Benzer şekilde Sınıf reducer'ımızı da oluşturalım.

redux/reducers/sinifReducer.js

Redux store hazır. Şimdi Redux'ı React içerisinde nasıl kullanacağımıza bakalım. React nesnelerini aşağıda pek de gurur duymadığım bir şekilde dizayn ettim. Öğrenciler bileşeni, öğrenciler reducer'ına bağlı. Ad ve Soyad metin kutuları ile Ekle düğmesi, eylem yayınlayan (action dispatch eden) elementlerimiz.

React-Redux bağlantısı

Metin kutuları onChange durumlarında Öğrenci reducer'ına OGRENCI.DEGISTIR_AD, OGRENCI.DEGISTIR_SOYAD eylemlerini yayınlarken; Ekle düğmesi de "OGRENCI.EKLE" eylemini gönderiyor. Dikkat edilirse "OGRENCI.EKLE"'nin payload'ı yok. Pek çok durumda payload'a ihtiyaç duyacağız ancak zorunlu değil. type" ise daha önce bahsettiğimiz gibi zorunlu. Redux, bir eylem yayınlandığında bütün reducer'larına bu eylemi dağıtır. Eğer Sınıf reducer'ına breakpoint eklerseniz Öğrenci reducer'ı için yayınladığımız eylemleri bile yakaladığını göreceksiniz. Ancak "type" değeri gönderilen action.type ile uyuşmayacağı için reducer bu eylem için işlem yapmayı es geçecektir.

components/Ogrenciler.js

Dikkat edilirse type karşılaştırması String değişkenleri ile yapılıyor. Yazım yanlışı yapılması durumunda, uygulamanızda nedenini hemen anlayamayacağınız bazı sorunlar yaşayabilirsiniz. Bu durumun önüne geçebilmek ve daha modüler bir programlama için genellikle tüm değerlerin bulunduğu bir "sabitler" dosyası eklenir.

constants.js

Daha sonra case 'OGRENCI.DEGISTIR_AD' yerine case constants.OGRENCI.DEGISTIR_AD gibi daha güzel bir yöntem kullanabilirsiniz. Ben bu örnek için böyle bir yapı kullanmadım; fakat, çalıştığım projelerde olmazsa olmaz bir yöntem bu.

Öğrencilere benzer şekilde Siniflar bileşenimiz var. Ancak, halen Redux bağlantısının nasıl yapıldığından bahsetmedim. import { connect } from 'react-redux'; ifadesinden de anlaşılacağı üzere React bileşenini Redux store'a bağlamak için react-redux'ın connect metodunu kullanıyoruz. Kimi durumlarda state'i güncellemek, kimi durumlarda ise state'deki verileri almak isteriz. connect'in ilk parametresi okumak ikinci parametresi ise yazmak için gerekli metodları bize veriyor.

connect(mapStateToProps, mapDispatchToProps)

mapStateToProps adından da anlaşılacağı üzere Redux'ın istediğimiz herhangi bir reducer'ındaki state'i, React elementinin prop'u gibi kullanabilmemizi sağlıyor. mapDispatchToProps ise benzer şekilde eylem props'una (onChange gibi) denk düşüyor. Burada global state üzerindeki herhangi bir değeri props'a atayabilirsiniz.

components/Siniflar.js

Bu basit uygulamanın tam halini aşağıdan edinebilirsiniz. "npm install" ve "npm start" komutları ile çalıştırılabilir.

https://gist.github.com/aliustaoglu/6ef24edb64a863fdc0020d401a563a57/raw/18f6c23eb5e25d999403461d2393a2bf2f2a1d1e/redux-uygulamasi.zip