RecoilJS Nedir? Redux ve MobX’in Yerini Alabilir mi?
Önceki yazımda React uygulamalarındaki state paylaşım probleminden ve Redux’ın bu problemi nasıl çözdüğünden bahsetmiştim. Redux’un büyük olduğu ve boilerplate kod ürettiği aşikar. Bu nedenle JS ekosisteminde de birçok farklı state management kütüphanesi bulunuyor. Hatta tüm state kütüphanelerinin bi arada bulunduğu awesome reposu bile var: https://github.com/olegrjumin/awesome-react-state-management
Hâl böyle olunca, “Yeni bir state kütüphanesine ne gerek var?” diyebilirsiniz. Bu söyleminizde haklı da olabilirsiniz. Fakat RecoilJS’i diğer state kütüphanelerinden ayıran en önemli fark, global state’i yüksek performanslı bir şekilde uygulamadaki bileşenlere dağıtmasıdır. Bu nedenle RecoilJS’i en basit haliyle tanımlamak gerekirse, verimli bir şekilde bileşenler arası state paylaşımının yapılması için Facebook’un üreetmiş olduğu bir state yönetim kütüphanesidir diyebiliriz. RecoilJS, 14 Mayıs 2020’deki ReactEurope etkinliğinde, David McCabe tarafından duyurulmuştur.
Bu yazımda da etkinlikten aldığım önemli notları size aktaracağım. Ayrıca yapacağım küçük bir uygulamayla da RecoilJS’i kendi uygulamanıza nasıl entegre edebileceğinizden bahsedeceğim. Yapacağım TODO uygulamasının bitmiş halini buradaki sandbox’tan çalıştırabilirsiniz: https://codesandbox.io/s/github/ozcanzaferayan/recoil-sample
Ortaya çıkışı
David McCabe, Douglas Armstrong ve Christian Santos tarafından üretildi. Dave’in aktardığı bilgiye göre RecoilJS, Facebook’ta Comparison View adlı veri analizi uygulaması için yapılmıştı.
Bu araç istemci ve sunucu tarafındaki performans bazında tıkanıklıkların analiz edilmesi için görsel bir arayüz sunuyordu. Bu uygulamada o kadar fazla grafik ve etkileşimli bileşen vardı ki, Context API, Redux ve diğer mevcut state kütüphaneleri iş göremez hale geliyordu. Bu kütüphaneler, ya istedikleri kadar esnek değildi, ya aradıkları performansı sunmuyordu, ya da mevcut state kütüphanelerinden birini kullandıklarında kodlar hataya meyilli hale geliyordu.
Bu nedenle state yönetimini uygulama içerisinde farklı bir şekilde gerçekleştirdiler ve yaptıkları çözümü projeden ayırıp bir kod kütüphanesi (RecoilJS) haline getirdiler. Bu kütüphane, genel olarak karşılaşılan 3 problemin çözümünü üzerinde durmaktadır:
1. State’in esnek bir şekilde paylaşılması
State’in esnek paylaşımı için, bir React uygulamasında, bileşen ağaçlarında farklı dallarda yer alan bileşenlerin, aynı state’i bilgisini benzer şekillerde görüntülemek amacıyla, performanslı bir şekilde senkronize etmesi olarak yorumlayabiliriz.
Örneğin aşağıdaki gibi bir çizim uygulamasında bileşen ağacında kırmızı renkteki bileşen Megaman karakterinin hareket etmesini sağlasın. Yeşil bileşen de megaman’in koordinatlarını konsola yazsın. Bu bağlamda kırmızı ve yeşil bileşenin aynı state üzerinde performanslı bir şekilde okuma/yazma işlemleri yapabiliyor olması gereklidir. Aksi takdirde farklı dallarda bulunan iki bileşenin aynı anda güncellenmesi, gereksiz şekilde tüm React ağacının tekrar render edilmesine yol açabilir.
2. Verinin türetilmesi (derived state) ve sorgulanması
State üzerinde verimli bir şekilde hesaplamaların yapılması ve bu şekilde state’in türetilmesi gerekmektedir. Bu işlem son derece performanslı olmalı ve herhangi bir hataya izin vermemelidir. RecoilJS’te state verisi atom
‘da saklanır ve state türetilmesi işlemi de selector
fonksiyonları aracılığyla verimli bir şekilde gerçekleştirilir. Yazının ilerleyen kısımlarında bu konuya detaylı şekilde değineceğiz.
3. Uygulama genelinde state’in izlenmesi
Oluşturulan state management kütüphanesinin state’i izleyecek şekilde aşağıdaki özellikleri içeriyor olması gerekir.
- State üzerinde zaman yolculuğu yaparak debug edilebilme.
- Yapılan işlemi geri alabilme.
- State’in kalıcı olarak saklanması.
- State değişikliklerinin log’lanması.
Bütün bu problemlerin çözümü için RecoilJS ortaya çıktı.
React’teki mevcut durum neden ihtiyaçları karşılamıyor?
Bu durumun birkaç nedeni var:
- React DOM ağacında alt dallardaki bir bileşenin state’i üst dallarda paylaşılabildiği için, üst daldaki bir state verisi değiştiğinde ilgili bileşene bağlı diğer alt bileşenlerin de gereksiz olarak tekrar render edilmesine yol açıyor.
- React Context yalnızca tek bir değer alabilmektedir. Birden fazla değer almalı ve bu değerleri dinleyen subscriber’lar ile birlikte tutabilmelidir.
- Bu iki nedenden dolayı, state’in yer aldığı React ağacının en tepesindeki bileşen ile state’in kullanıldığı ağacın yapraklarındaki bileşenleri birbirinden ayırmak (code-splitting) zorlaşmaktadır.
RecoilJS, state’i React DOM ağacından ayırarak, yönlü grafik (directed graph) şeklinde saklamakta ve DOM ağacına kolayca takılabilir hale getirmektedir.
State değişiklikleri, bu grafiğin root elemanlarından (atomlardan) bileşenlere doğru fonksiyonlar (selectors) aracılığıyla aktarılır. Bu yaklaşım sayesinde aşağıdaki imkanlar oluşmaktadır:
- Paylaşılan state’in React’teki
useState()
hook’una benzer şekildeuseRecoilState()
hook’u ile yönetilmesi sayesinde kod kalabalığı olmayan (boilerplate’siz) bir API üretilir. - Mevcut state yönetimi, Concurrent Mode ve diğer yeni React API’leri ile uyumlu hale gelir.
- State tanımı dağıtık bir şekilde ilerlediği için, web uygulamalarında kodun ayrı bir dosya halinde sunucuya konulması (code-splitting) mümkün olmaktadır.
- State, kendisini kullanan bileşenleri değiştirmeden, türetilen veri ile selector’ler aracılığıyla, kolayca yer değiştirilebilir.
- Türetilen veriler, kendisini kullanan bileşenleri değiştirmeden, senkron veya asenkron hale getirilebilir.
- Uygulamanın state’i, URL içerisine kodlanarak, bir link vasıtasıyla state’in paylaştırılması kolayca yapılabilir.
- Uygulamanın state’i önceki versiyonla uyumlu olacak şekilde kaydedilebilir. Böylece uygulama değiştiğinde, kaydedilmiş state tekrar kullanılabilir.
Temel kavramlar
RecoilJS sayesinde uygulama verileri, atom’lardan oluşan paylaşılan state’te tutularak, selector fonsiyonlar aracılığıyla React bileşenlerine aktarımı sağlanır. Bileşenlerin abone olduğu state parçalarına atom adı verilir. Selector‘ler ise state’i senkron veya asenkron olacak şekilde değiştiren fonksiyonlardır.
Atomlar
Atomlar en küçük state birimleridir. Güncellenebilir ve abone olunabilirler. Bu sayede atom güncellendiğinde, kendisine abone olan bileşen, aldığı yeni değer ile tekrar render edilmektedir. Bununla birlikte atomlar, runtime’da (uygulamanın çalışması esnasında) da oluşturulabilirler. Atomlar, bileşen içerisine yerleştirilerek ve aynı bir local state gibi kullanılabilirler. Eğer bir atom, birden fazla bileşen tarafından kullanılırsa, o bileşenler state’i paylaşmış olurlar.
Atom’lar atom()
fonksiyonu ile oluşturulurlar:
Atom’ların benzersiz bir key
değerine ihtiyacı vardır. Key değeri; state’i debug yapmak, verileri hafızada tutmak (persistance) ve atom’ları map etmeyi sağlayan diğer gelişmiş API’lerin kullanımı için gereklidir. Eğer iki atom aynı key değerine sahipse, uygulamada hata oluşabilir. Bu nedenle verdiğiniz key değerinin uygulama genelinde benzersiz olmasına dikkat etmeniz gereklidir. Atomlar, React state’i ile benzer oldukları için, varsayılan olarak da bir değer almaktadırlar.
Bir bileşen içerisinden atom değerinin alınıp okunabilmesi için useRecoilState()
hook’u kullanılır. Bu hook, React’in useState()’i gibi çalışır. Tek farkı, state artık diğer bileşenler de ile paylaşımlı halde yer almaktadır:
FontButton
bileşeni butona tıklandığında font büyüklüğünü arttırmayı sağlar.
fontSizeState atomu diğer bir bileşen olan Text
‘te de aşağıdaki gibi kullanılabilir.
Kodu çalıştırdığınızda, metin de buton da aynı state’i paylaştığı için aynı şekilde büyütülecektir:
State’in bileşenler arasında paylaştırılması bu kadar kolaydır. Şimdi paylaşılan state’ten verinin türetilmesi ve kullanılması için selector’lerin nasıl kullanılacağına değinelim.
Selector’ler
Selector’ler girdi parametresi olarak atom veya başka bir selector alırlar. Daha sonra ilgili atom bilgisinden değiştirip türeterek, üzerinde yalnızca okuma işlemi gerçekleştirilebilen bir state oluştururlar. Bu sayede benzer state bilgisi için birden fazla atom oluşturmak yerine, bir atom oluşturularak selector’ler aracılığıyla ilgili veri türetilerek kullanılır. Aşağıdaki örnekte, fontSizeState
atomundan türetilerek bir font stili elde edilmektedir:
Oluşturulan selector, daha sonra bir state gibi kullanılabilir. Bunun için useRecoilValue()
hook’undan yararlanılır:
Artık tıklama işlemi ile buton ve metnin boyutunu arttırmakta ve div elemanı içerisine ilgili font büyüklüğünü yazdırılmaktadır.
Genellikle geliştirdiğimiz uygulamalarda bu şekilde tekil bir değer yerine bir array tutacağımız için şimdi örnek bir todo uygulaması yaparak bunu deneyimleyelim.
Basit bir TODO uygulaması yapımı
Öncelikle bir react uygulaması oluşturalım:
npx create-react-app recoil-sample
Ardından proje içerisine giderek recoil kütüphanesini ekleyelim:
cd recoil-sample
yarn add recoil
Devamında projeyi vscode’da açalım ve sunucu üzerinde ayağa kaldıralım:
code .
yarn start
RecoilJS ile state’leri yönetebilmek için <App>
bileşenini <RecoilRoot>
bileşeni ile çevrelemek gerekiyor. Bunun için index.js dosyasını aşağıdaki gibi değiştirelim:
Sonrasında TodoList’in görüntülenmesi için öncelikle App.js
dosyasına gelelim ve recoil’i import ederek todoListState
atomunu oluşturalım:
Daha sonra state’teki bu elemanları görüntülemek için useRecoilValue()
hook’unu kullanalım ve App.js
‘i aşağıdaki gibi hale getirelim:
Kodun çıtkısı tarayıcıda aşağıdaki gibi görüntülenecektir:
Şimdi state’e eleman ekleme işlemine geçelim.
Ekleme (add) işlemi
Ekleme yapmak için global state üzerinde değişiklik yapmamız gereklidir. useRecoilValue()
hook’u da sadece state’in okunmasını sağladığı için bunun yerine useRecoilState()
hook’unu kullanabiliriz. Bu hook’un kullanımı useState()
ile aynı şekildedir:
Ayrıca eklenecek elemanı da useState ile tutmamız gerekiyor:
todo
elemanının değerini değiştirmek için aşağıdaki gibi <input>
kullanabiliriz:
Daha sonra todo
elemanını todoList’e eklemek için bir <button>
kullanabiliriz:
App.js
‘in son hali aşağıdaki gibi olacaktır:
Uygulamayı çalıştırdığınızda aşağıdaki gibi listeye eleman ekleyebilirsiniz:
Ekleme işlemi tamamlandığına göre şimdi silme işlemine geçebiliriz.
Silme (delete) işlemi
İlgili satırdaki liste elemanını silmek için aşağıdaki gibi bir deleteItemAt(index)
fonksiyonu oluşturabiliriz:
Oluşturduğumuz fonksiyonu kullanabilmek için öncelikle ilgili elemanın index’ini almamız gerekiyor. Bu nedenle map fonksiyonu içerisindeki 2. parametre olan index
‘i kullanabiliriz. Devamında <li>
elemanının yanına bir buton ekleyerek ilgili index’teki elemanı silebiliriz:
App.js
‘in son hali aşağıdaki gibi olacaktır:
Uygulamayı çalıştırdığınızda aşağıdaki gibi listedeki herhangi bir elemanı silebilirsiniz:
Silme işleminden sonra da düzenleme işlemine geçebiliriz
Düzenleme (Edit) işlemi
İlgili satırdaki liste elemanını düzenlemek için, bir prompt dialog görüntüleyecek şekilde aşağıdaki gibi bir editItemAt(index)
fonksiyonu oluşturabiliriz:
Bu fonksiyonu kullanmak için listeye bir buton ekleyebiliriz. App.js’in son hali aşağıdaki gibi olmalıdır:
Aşağıdaki gibi herhangi bir elemanı düzenleyebilirsiniz:
Şimdi liste elemanının üstüne tıklandığında tamamlandığını belirtecek şekilde üstünü çizme işlemine geçebiliriz:
Todo elemanının tamamlanması (complete) işlemi
İlgili elemanın üstüne tıklandığında tamamlandığını belirtmek için üstünü çizebiliriz. Bu nedenle her liste elemanı için isCompleted gibi bir değere ihtiyacımız var. Fakat todoListState
atomu string array’inden oluştuğu için öncelikle object array haline getirip isCompleted
alanını eklemek gerekiyor:
State artık bir object array haline geldiği için uygulama hata verecektir. Bu nedenle todoListState’i kullanan diğer kodları da değiştirmemiz lazım. Değişiklikleri yaptığınızda App.js aşağıdaki gibi olacaktır:
Şimdi liste elemanının üstüne tıklama işlemi için {item.name}
‘i <span>
ile çevreleyebiliriz
Şimdi completeAt
fonksiyonunu yapalım:
App.js
‘in son hali aşağıdaki gibi olacaktır:
Kodu çalıştırdığınızda ekran görüntüsü aşağıdaki gibi olacaktır:
Listenin filtrelenmesi işlemi
Oluşturduğumuz todo uygulamasında biten/bitmeyen/tüm maddeler bazında filtreleme yapmak isteyebiliriz. Bunun için öncelikle filtre değerini uygulama bazında tutmak için bir atom oluşturalım:
Şimdi oluşturduğumuz atomu bileşen içerisinde useRecoilState()
fonksiyonu ile filter değişkenine atayalım:
Artık <input>
elemanının üstüne aşağıdaki gibi bir select elemanı ekleyerek filtre değerini değiştirebiliriz:
Filtrenen elemanları da harici bir değişkenden almak gerekiyor. Bu işlem için ayrı bir state’te array tutmak gereksiz olacaktır. Çünkü elemanları todoListState’ten türeterek edinebiliriz. Bu nedenle tarz türetilmiş veriler (derived state) için daha önce de anlattığımız gibi selector’leri kullanabiliriz. Şimdi filteredTodoListState
‘i bir selector vasıtasıyla oluşturalım:
Şimdi bu filteredTodoListState’i bileşen içerisinde kullanalım. selector değeri sadece okunabilir olduğu için useRecoilValue()
hook’unu kullanabiliriz:
Filtrenen elemanları ekrana bastırmak için todoList
yerine filteredTodos
değişkenini kullanmamız yeterli olacaktır:
{todoListState.map((item, index) =>
//yerine
{filteredTodos.map((item, index) =>
Değişiklikler yapıldığında ve gerekli fonksiyonlar import edildiğinde App.js
‘in son hali aşağıdaki gibi olacaktır:
Ekran görüntüsü aşağıdaki gibi olacaktır:
Uygulama kodu artık aşırı büyüdüğü için refactor etme işlemine geçebiliriz.
RecoilJS kodunun refactor edilmesi
Kodun refactor edilmesi için atom ve selector’leri ayrı dosyalara taşıyabiliriz. bunun için src dizini içerisinde state
isimli bir dizin oluşturalım. Sonrasında atoms.js
ve selectors.js
dosyalarını aşağıdaki gibi ekleyerek ilgili elemanları taşıyalım: src/state/atoms.js:
src/state/selectors.js:
Ayrıca uygulamayı 3 mantıksal bileşene ayırabiliriz:
- <TodoListFilters />: Filtre işlemlerinin gerçekleştiği bileşen.
- <TodoItemCreator />: Todo elemanı ekleyen bileşen.
- <TodoItem />: Görüntülenecek olan todo elemanı.
Bunun için src dizini içerisinde components
isimli bir dizin oluşturarak 3 bileşeni taşıyalım:
- src/components/TodoListFilters.js
- src/components/TodoItemCreator.js
- src/components/TodoItem.js
App.js’in de son hali aşağıdaki gibi olacaktır:
Network üzerinde işlem yapılması
Verilerin asenkron olarak getirilmesi için selector’lerden faydalanabiliriz. Örneğin github’daki bir kullanıcının bilgilerinin getirilmesi için aşağıdaki gibi bir selector oluşturabiliriz:
- src/state/selectors.js
Bu selector’ü kullanacak olan UserInfo bileşeni de aşağıdaki gibi yazılabilir:
- src/components/UserInfo.js
App.js içerisinde de aşağıdaki gibi React Suspense ile kullanılabilir:
Kodu çalıştırdığınızda aşağıdaki gibi Loading… yazısı görüntülenecek, ve veriler indiğinde artık ilgili github kullanıcısının adı ekrana basılacaktır:
Sonuç olarak
Kendi açımdan baktığımda, RecoilJS’in minimal olması ve kolayanlaşılırlığı ile Redux ve MobX’e göre daha kolay uyum sağladım. Henüz deneysel olan bu kütüphanenin zaman içindeki gelişmelerini birlikte izleyerek hangi noktalara geleceğini görelim. Projenin kodlarına buradan ulaşabilirsiniz: https://github.com/ozcanzaferayan/recoil-sample
Bir sonraki yazıda görüşmek üzere…