İpin ucunu kaçırmamak: Redux
Son yıllarda frontend framework’lerin sayısı giderek artıyor ve popülerleşiyorlar. Özellikle yeni başlanan her proje Single Page Application (SPA) ile oluyor artık. Angular, ReactJs ve daha nice frontend framework hayatımızı kolaylaştırmak için vitrinlerde yerini almış durumda ve hızla gelişmeye devam etmekteler.
FAKAT!
Bir problem var!
Benim şimdiye kadar karşılaştığım SPA framework’leri, tek bir root component’in, birden fazla component (child, inner ya da sub components) barındırması ile oluşuyor. Ve Reactjs benzeri, data’nın (model, state) bu component’ler arasında dolaştırıldığı framework’lerde, bu data’nın yönetimi gerçekten zor olabiliyor. Aynı data (model), farklı component’lere geçildiği için, kimin data’yı manuple ettiğini bilemeyebiliyoruz.
Şimdi bunu, mockup’ını oluşturduğum bir örnek üzerinden anlatmaya çalışayım.
Burada, bir web application’ın component tasarımını görüyorsunuz. Mor, açık mavi ve iki tane de gri component olmak üzere toplamda 4 ayrı komponent var. Ve root component’imiz, hepsini kapsayan mor komponent.
Application ilk ayağa kalktığında user, veritabanından çekiliyor ve sağ üste yazılıyor. Daha sonra aynı user, içerdeki component’lere pass ediliyor. Bir kez çektiğim data’yı bu şekilde kullanmak akıllıca aslında. İçerdeki component’ler için ayrı ayrı http-get yapmama gerek kalmıyor ve performanstan kazanıyorum.
Uygulamamızın bir özelliği daha var ki, “fakat”ın başladığı yer oluyor burası. Yukardaki textBox’ların içindeki değeri değiştirdiğimizde, kaydete tıklamadan, kırmızı label’da yazıldığını (unidirectional data flow) görüyoruz. Bu durumda Javascript’te mutable olarak tanımlı object ile, root component’in sağ üstündeki isim de değişmiş oluyor. Yani siz textBox’ı değiştirdikçe, henüz kaydetmemiş olmanıza rağmen en dıştaki root component de bundan etkileniyor ve kaydedilmiş gibi davranıyor.
Javascript’te immutable ve mutable kavramı
Bu kavramları uzun zamandır duymuyorduk. Facebook, Reactjs ile bastırdıkça tekrar önem kazanmaya başladı. Bu kavramlara hakim olmadan, Reactjs veya React-native gibi framework’lerde ilerlemek, büyük projeler yapmak oldukça güç. Çünkü her ne kadar bu framework’ler hayatı kolaylaştırmak için yapılmış olsalar da, proje büyüdükçe complexity’nin artma riskini de içlerinde barındırıyorlar.
Şimdi müsadenizle bu kavramları biraz daha açmam gerekiyor.
Javascript’te tipler (type), yapısal olarak ikiye ayrılırlar.
- Value types
- Reference types
Value type’lar, primitif tiplerdir. Undefined, boolean, number, string gibi tipler bu kategoridedir. Bu tiplerin özelliği immutable olmalarıdır. Bir kere yaratıldıktan sonra asla değişikliğe uğramazlar.
var a = 10;
var b = a;
a = 20;
console.log(a); //20
console.log(b); //10
Burada a’yı, b’ye eşitlediğimiz yerde, a ile b’nin referansları eşitlenmez, bunun yerine a’nın bir kopyası oluşturulur, ram’de yeni bir yer allocate edilir ve artık orada b vardır. Bu sayede a=20 dediğimizde b’nin değeri 10 olarak kalmaktadır. Her şey gayet güzel ve beklendiği gibi!
Object ve array ise reference type’lardır ve mutable’dırlar. Bir eşitleme yapıldığı durumda, ram’deki referans adresleri eşitlenir. Bu nedenle, herhangi bir değişiklikte, aynı adrese eşitlenmiş diğer değişkenin de değeri değişir.
var a = {name:”Onur”, age:30}
var b = a;
a.age = 25;
console.log(a); //{name:”Onur”, age:25}
console.log(b); //{name:”Onur”, age:25}
İki değişkenin de age değeri 25'e setlenmiş durumda. Daha da doğrusu, iki değişkenin de ram’de adreslenmiş olduğu alandaki age değeri 25 olmuş durumda.
Javascript’te mutable olan değişkenleri, immutable yapmak için birçok ürün var. Immutable.js, seamless-immutable bunlardan bazıları. Bunları kullanmak istemezseniz, object’ler için object.assign ve array’ler için concat, filter gibi metodları kullanarak, immutable yapmak da mümkün.
Yukarda verdiğim örneği şu şekilde yazsaydık, immutable bir b elde etmiş olurduk:
var a = {name:”Onur”, age:30}
var b = Object.assign({}, a);
a.age = 25;
console.log(a); //{name:”Onur”, age:25}
console.log(b); //{name:”Onur”, age:30}
Artık elimizde adresleri birbirinden farklı iki reference type var ama immutable’lar artık.
Ya da (ECMAScript 2015 ya da ES6) spread operator’ünü kullanabiliriz:
var a = {name:”Onur”, age:30}
var b = {…a};
a.age = 25;
Sonuç yine aynı olacaktır. Bu üç noktanın (speread operator) yaptığı, a’nın içindeki tüm node’ları (property’leri) özyinelemeli (recursive) olarak açarak (unpacking), b için yarattığı yeni bir object’in içine eşitlemek (set). Aynı şekilde array’ler içinse:
var a =[10,20,30];
var b = {…a};
a.push(40);
şeklinde kullanabiliyoruz spread operator’ünü.
Şimdi yukardaki örneğimizi düşünürsek, mutable type’larla gittiğimizde başımıza nelerin gelebileceğini tahmin edebiliriz.
Hatta Facebook’un Reactjs’i çıkarma nedeni de tam bu yüzden oluyor. Belki aranızda hatırlayanlarınız vardır, bir dönem Facebook’ta yukardaki gelen mesaj notification sayısı ile, mesaj içeriğindeki mesaj sayısı tutmuyordu. Yukarda kırmızı 1 görüp koşuyorduk ama aslında hiç mesaj olmadığını görüyorduk. İşte arkadaşlar o dönem yaşanan tam olarak mutable problemiydi.
Çünkü devasa uygulamalarda, context her yere geçilmektedir. Bir yılın sonunda, “Allah Allah bu context’i buraya kim geçti” fıkrasını anlatacak kıvama gelebilirsiniz.
İpin ucunu kaçırmadan Redux’a geri dönelim
İşte Redux’ın motivasyonu da biz geliştiricileri immutable olmaya zorlamasıdır. Çünkü yukardaki örneğimizin aksine, nested birçok object barındıran bir state object’imiz olabilir. Ve bu object’i bir function’a parametre olarak geçiyor olabiliriz. Peki bu object’in değiştirilip değiştirilmediğini veya değişikliğin doğru yapılıp yapılmadığının bir garantisini verebilir miyiz? Hayır. Çünkü herhangi bir takip mekanizmamız ve kontrolümüz yok mutable tip’ler üzerinde. Belki de uygulamanın bambaşka bir yerinde değiştirilecek bir başka değişkene eşitlendi ve kimbilir kim, nasıl, ne zaman değiştirecek bu yeni değişkeni. Yani artık nesnemiz kestirilemez (unpredictable) hareket etmeye başlıyor. Nerede, nasıl, kim değiştirdi, ne zaman değiştirdi soruları yanıtsız kalıyor. Uykusuz debug’lar başlıyor.
Redux hakkında bilmeniz gereken bir diğer şey ise, React ile organik bir bağının aslında olmaması. Yani alıp, plain Javascript bir projede de kullanabilirsiniz. Yeter ki mimariniz el versin.
Mimari kısmınından yukarda bahsetmiştim. Şimdi biraz daha açayım. Tek bir root component’in olduğu ve state’leri iç component’lere (sub components) geçebilme olanağı tanıyan framework’lerde, model (state, data, object), view’a (html olarak düşünebiliriz şimdilik) bağlıdır (loose veya tight olması konumuz dışında). Ve view üzerinde yapılan değişiklik model’a yansıtılabilir. Aynı model, farklı bir view’a geçilmiş olabilir ve bu model’deki değişiklik farklı bir view’ı tetikleyebilir. Derken birbirini tanımayan iki view’ı istemeden değiştirmiş olursunuz.
Şimdi binlerce view’ınızın (component) ve binlerce sub model’inizin olduğunu hayal edin. Yeni özellik (feature) eklemeye korkar hale gelir insan.
Mutfağa geçelim
State management (durum veya model yönetimi diyebiliriz) kavramının zorluklarını anladığımıza göre artık kod yazmaya başlayabiliriz.
Projeyi oluşturmak için özel bir şey yapmadım. Boş bir klasörün içinde npm init çalıştırdıktan sonra, npm i redux -S komutunu çalıştırdım sadece. Bir de kodları yazmak için index.js dosyası oluşturdum. Sonuçları görmek için ise node index.js komutunu terminalden çalıştırmak yeterli olacaktır.
Redux’ta üç ana konsept bulunmaktadır.
- Actions
- Reducers
- Store
Store, application’ımızın ihtiyaç duyduğu state’i ifade etmektedir ve application lifecycle’da yalnızca bir tane olmalıdır. Bunu, veritabanımızın tamamı olarak veya global bir değişken olarak hayal edebiliriz.
Reducers’ın ise veritabanımızdaki her bir tabloyu temsil ettiğini düşünebiliriz. User, addresses gibi tabloları, reducers olarak hayal edebilirsiniz.
Actions ise tablolar üzerindeki her bir işlemi temsil eder. User’ın name’ini değiştirmek bir action’dur. Yeni bir address eklemek bir action’dur.
Yapacağımız örnekte, state’te tuttuğumuz bir kullanıcı üzerinden immutable nasıl olabiliriz bunu göreceğiz. Şimdi buradan daha öteye gitmeden önce bir uyarıda bulunmak istiyorum. Bu yazıyı yazmamın amacı, neden Redux kullanmalıyıza açıklık getirmekti. Bu yüzden müsadenizle sizi hızlıca http://redux.js.org/ sayfasına yönlendirip oradaki INC/DEC örneğini incelemeye davet ediyorum. Özel bir şey yapmanıza gerek yok, kodları okumanız yeterli olacaktır. Her şey ultra basit yapılmış. Tek sorun ilk örnekte immutable bir veri tipi kullanılıp “ee ne işe yaradı bunu böyle yapmak” gibi bir soru işareti bırakması. Siz bu örneği okuyup döndüğünüzde ben de size mutable tiplerde kullanımı ve bize kazandırdığı avantajları gösteren bir demo hazırlayacağım.
Tekrar hoş geldiniz. Nasıldı? Hmm immutable’dı. Şimdi mutable bir örnek yapalım birlikte. Öncelikle modelimizi bildiğimiz için buna uygun reducer’ı yazıyoruz.
Gördüğünüz gibi user.name ve user.age’te değişiklik yapmak için iki ayrı statement oluşturuyoruz. State’te değişiklik yapıp bunu return ediyoruz. Action ise model’de yapılacak herbir şeyi temsil etmektedir. Update mi ediyoruz, delete mi ediyoruz, update ediyorsak yeni data nedir, delete için hangi id verilmiş… Tüm bu bilgileri action üzerinden aktarıyoruz. Reducer’ların state ve action parametrelerini barındırması zorunludur.
Burada dikkat edilmesi gereken bir nokta da state’i, her zaman bir öncekinin değerlerini alarak yeniden oluşturmamızdır. Yani nesnemiz immutable olarak geri dönüyor ki, döndüğümüz yerde bir değişiklik yapıldığında, application’un her yerinde gezecek olan state’i etkilemesin. İşte tam bu noktada, immutable.js gibi ürünleri kullanmak da mümkündür (fakat bu ürünlerin de bazı problemleri barındırdıklarını unutmayalım, araştırıp, deneyip, tüm ekiple bir araya gelip öyle karar verelim).
Şimdi, tüm application’da dolaşacak store’umuzu oluşturuyoruz. Yazının önceki alanlarında da belirttiğim gibi, tek bir state (store) olması gerekmektedir. Yani state’imizi tutacak olan store, global olacak.
Buradaki middleware’e daha sonra değineceğim. Şimdi bunları nasıl kullanıyoruz görelim:
Burada görmekte olduğumuz parametre’ler çok önemlidir. Özellikle type zorunlu bir parametredir. İkinci geçilen parametre ise sizin tasarımınıza kalmış. Burada olduğu gibi payload geçerseniz reducer içersinden action.payload diye karşılamanız gerekir; payload:{name:”Ahmet”} şeklinde geçerseniz action.payload.name diye ulaşırsınız geçilen parametreye.
Bu sayede, user modelimize direkt erişimi ortadan kaldırmış oluyoruz. Genel hatları itibari ile örneğimizi anlattığımıza göre, tüm kodu paylaşabilirim:
https://github.com/onuar/reduxdemo
Şimdi sizden ricam, index.js’i incelemeniz. Ben burada bekliyorum.
Tekrar selam. Yukarda da gördüğümüz, middleware’lerden de kısaca bahsedeyim. Middleware’lere aslında birçok alandan aşinayız. Nodejs, Asp.Net MVC, Python Flask gibi bir çok web framework’ünde middleware’ler mevcut. Bu arkadaşlar, request-response pipeline’ında araya girmemizi (intercept) ve hatta çalışma anında pipeline’a müdehale etmemizi sağlıyor. Burada da durum aynı. Her bir action çağırıldığında, öncelikle logger middleware’imiz çağırılıyor, next(action) diyerek bir sonraki execution’a paslanıyor. Bu örneğimizde error middleware’i oluyor bu. Error middleware’inde ise try bloğu içersinde next(action) çağırılıyor ki bu da gerçek action’umuz oluyor. Burada bir hata oluşursa, ekrana hata logu olarak bastırılıyor.
Koddan çıkmadan önce, bu kod ile biraz oynamanızı, bozmanızı, “hmm böyle yaparsam nolur acaba” diye sormanızı rica ediyorum. Readme’ye eklediğim küçük bir “hack and see” bölümü bir fikir verebilir bu noktada. Bu değişiklikleri yapmak Redux’ı daha iyi anlamanıza yardımcı olacaktır.
Sonuç
Redux’ün üç temel prensibi bulunmaktadır.
- Tek state (yani tek store)
Bunu, createStore diyerek yapıyoruz. Bu function, birden fazla reducer’ı combine edip yine tek bir store dönmektedir. Yani ikinci bir modelimiz olduğunda da yine elimizde tek store olacaktır. Bu store’u, uygulamamızın istediğimiz component’ine rahatlıkla gönderebiliriz. - State, değiştirilemez!
Yani elini kolunu sallayan, direkt object üzerindeki property’lere erişerek değişiklik yapamaz. Her şey reducer’lar üzerinden gider. Bu sayede kontrol altındadır. Immutable olmayan kimseyi reducer’dan return etmeyiz! Ayrıca middleware’ler veya subscriber’lar sayesinde gelen, state’in geçmişini (history) tutmak da mümkündür. T zamanda istediğiniz state’e dönebilirsiniz bu sayede. - Değişiklik eşittir Basit function’lar
State’teki her yapılan değişiklik, basit birer function’dan ibarettir. Yani gerçek hayat örnekleri burada yaptığımız örneğin çok ilersinde falan değildir. Bu sayede application karmaşık (complexity) bir hal aldığında, yönetilmesi zorlaşmaz.
Redux’ı kullanmayacaksanız bile, çözüm getirdiği problemleri unutmamakta ve bunlara yönelik önlemler almakta fayda var.
Okuduğunuz için teşekkür ediyorum.
Esen kalın.