NGRX - Component’a Özel State Kullanmak — 1
Global state’i local state olarak kullanmak ve global state’i dilimlemek
You can read this blog post in English here.
Bu yazımda angular ile birlikte ngrx kullananların sıklıkla karşılaşabileceği bir sorun olan component’a özel state kullanmak nedir, nasıl yapılır, ne gibi problemlerle karşılaşılır ve nasıl çözülür konularına değineceğim. Bu konu üzerinde bırakın türkçe kaynak bulabilmeyi ingilizce kaynak bile bulmak neredeyse imkansız. Sorun hakkında ngrx core geliştiricilerinin ya da contributor’ların paylaştığı bir kaç yazı bulmak ya da stackoverflow’da bu problem ile karşılaşmış kişilerin sorduğu soruları ve verilen cevapları incelemek mümkün olsa da çözüm bulabilmek epey zahmetli. Üstelik ngrx’i ve angular’ı iyi seviyede kavrayamamış iseniz bu yazıları anlamak neredeyse imkansız hale geliyor. Bu yazının amacı bu sorunu hem çözmek, hem türkçe bir kaynak yaratmak hem de mümkün olan en basit seviyede işleyerek az da olsa angular ve ngrx hakkında bilgi sahibi olanların konuyu net şekilde anlamasını sağlamak olacak.
Aslında 10 Ağustos 2020'de Ngrx’in 10. versiyonu ile birlikte duyurulan bu soruna odaklanmış olan bir modül (@ngrx/component-store) Google Firebase Console Team’in çalışanlarından Kevin Elko ve Alex Okrushko tarafından geliştirildi. Fikrin yaratıcısı Kevin olup ngrx’e entegre eden ise Alex’tir. Bu problemin kesin çözümünün bu modülü kullanmak olduğunu söyleyebilirim ancak yine de bazı nedenlerden dolayı global state’i local state olarak kullanmak isteyebilirsiniz. Bu sebeple bizim işleyeceğimiz konu bu yeni modül değil, bildiğimiz @ngrx/store’u kullanarak bunu nasıl yapabiliriz olacak.
Konuya giriş yapmadan belirtmem gerekir ki ngrx’i kullanmış ve mantığını biliyor olmanız gerekli. Bu nedenle yazının devamında orta seviyede angular, rxjs ve ngrx’e hakim olduğunuzu varsayarak ilerleyeceğim. Ngrx’te store, effects ve entity modüllerini kullanacağım. Daha önce ngrx kullanmamış iseniz okumaya devam etmenize gerek yok diyebilirim.
Sorunlar ve Çözümler
Öncelikle sorun olarak bahsettiğimiz durum neydi bunu bir tanımlayalım. Bildiğiniz üzere ngrx’te reducer ve effect’lerimizi module seviyesinde provide ediyoruz. İlgili modül yüklendiğinde ngrx bizim için reducer’larımızı ve effect’lerimizi register ederek initialState ile birlikte state’i kullanıma hazır hale getiriyor. Buraya kadar bir problem yok. Konuyu daha iyi anlayabilmek için kodlayarak devam edelim.
Basit bir örnek oluşturalım. PersonList adında bir component’ımız olsun ve bunu module olarak tasarlayalım. PersonList içinde bir kişi listesi bulundursun ve yeni kişi ekleme ve silme yeteneklerine de sahip olsun. Bu modulü root’da provide edelim ve istediğimiz sayfada istediğimiz kadar bu component’ı kullanabilmeyi amaçlayalım. Aynı zamanda bu component ngrx global state’i kullansın. Hazırladığımız örneğin çalışan halini denemek isteyenler için stackblitz‘te de paylacaşağım.
Örneğimizi çalıştırıp iki kişi eklediğimiz de global state’imiz şu şekilde görünüyor.
Kodlarımız ise şu şekilde:
Paylaştığım örnek şu an beklediğimiz gibi doğru çalışıyor. Bir kişi eklediğimizde addPerson action’ını dispatch ediyoruz ve effect ile servisimize gidip kaydetme işlemini tamamlıyoruz. Arkasından side-effect olarak addPersonSuccess effect’i çalışıyor ve tarayıcıdan “save success” alert’ini alıyoruz. Reducer ise bizim için global state’i güncelliyor. Selector’ler yardımı ile de async olarak verileri okuyup ekranda render ediyoruz. Şu andan itibaren problemi reproduce etmek için kodları değiştireceğiz ve ne gibi sorunlar ortaya çıkıyor hep birlikte göreceğiz. Adım adım ilerleyeceğiz ve nasıl çözümler üretebiliriz birlikte düşüneceğiz.
Sorun 1
PersonList component’ımızı farklı farklı sayfalarda birden fazla kez kullanabileceğimizi varsayalım. Ve her kullanımda state’inin temiz olmasını kayıtlı herhangi bir kişi bulunmamasını isteyelim. Page 1'i yenilediğimizde (F5) component tertemiz geliyor, bunda bir problem yok. Şimdi bir page 2 yaratalım ve bu component’ımızı aynı şekilde orada da kullanalım. Daha sonra page 1'de iken kişi ekleyelim ve page 2'ye geçelim. (Page 2'nin kodunu buraya yazmıyorum. Sadece stackblitz örneğini koyuyorum. Kod olarak page 1'in aynısı)
Eğer bahsettiğim gibi denerseniz page 2'de de page 1'de iken kaydetmiş olduğunuz kişiyi listede göreceksiniz. Hatta page 2'de de kişi ekleyip tekrar page 1'e dönerseniz page 2'de kaydettiğiniz kişileri de göreceksiniz. Bunun sebebi net şekilde belli. Component reusable olsa da kullandıkları state ortak. Şimdi öncelikle bu problemi çözelim. Page 2'ye geçtiğimizde state’imiz temizlensin. Bunun için live demo koymayacağım. Yukarıdaki örneği forklayarak kendiniz deneyebilirsiniz. Ben sadece kodları paylaşacağım.
Çözüm olarak çoğunuzunda aklına ilk gelen yöntemi uyguladık. Bir destroy action’u yarattık ve component her destroy olduğunda bu action’ı dispatch ettik. Reducer tarafında ise state’imizi initial state’e tekrar geri set ettik. Özetle tek olan state’imizi her seferinde yeniden kullanmaya hazır hale getirdik. Global state’i local state olarak kullanırken karşımıza çıkabilecek problemlerden bir tanesi buydu.
Sorun 2
Şimdi gelin işleri biraz daha karmaşık hale getirelim. PersonList component’ını aynı sayfada iki defa kullanalım ve 2. sorunumuzla başbaşa kalalım :)
Yukarıdaki görselde gördüğünüz gibi hangisinden kişi eklediğimiz farketmeksizin iki component’ta da eklediğimiz kişileri görüyoruz. Redux dev tools ile state’i kontrol edecek olur isek tek bir state olduğunu farkedeceksiniz.
Bu component farklı sayfalarda sadece birkez kullanılıyor olsaydı sorun 1'i çözdüğümüzde aslında işimiz kalmamıştı. Ancak aynı sayfada birden fazla kullandığımızda ortak state bize hala problem çıkarmaya devam ediyor. State’i her component’a özel tutarak bu problemi aşmamız gerekiyor. Peki bunu nasıl yapacağız?
Bu noktada component’lar için local state yaratmamız gerekiyor. Ancak bunu global state’i kullanarak yapacağız. Bunu nasıl yapacağımıza dair kaynak aradığımızda genelde karşımıza çıkan çözüm yöntemini deneyerek başlayalım.
İlk olarak action’lardan başlayalım. Dispatch olan action’ları birbirinden ayırmamız lazım. Bunun için tüm action’larımızın meta datasına identifier ekliyoruz. Bu identifier sayesinde reducer’a gelen action’ın hangi state için çalıştırılması gerektiğini ayırt edeceğiz.
Reducer’ımıza her bir PersonListState’ini saklayacak kapsayıcı bir state tanımlıyoruz ve identifier’ımıza göre state’i parçalıyoruz. State’lerimiz lists’in altında parçalar halinde duracak.
stateSlice adında yardımcı bir function hazırlıyoruz. Bu function bize identifier’ımıza göre state’in ilgili parçasını kesip verecek. Bu şekilde component’a özel state’i izole etmiş olacağız ancak reducer kodumuz şimdilik çirkinleşecek. Bu haliyle reducer yazmak hem vakit kaybettirici olur hem de okunabilirliği azaltır.
Örnek olarak sadece addPerson ve removePerson action’larının son halini koyuyorum. Daha detaylı incelemek isteyen stackblitz’den bakalabilir.
Selector’lerimize de aynı şekilde identifier config’ini ekliyoruz. Reducer’da olduğu gibi burada readability ve maintainability’i kaybediyoruz. Okunabilirliği arttırmak ve bakım maliyetini düşürmek adına selector’lerde ve reducer’larda farklı yöntemler deneyebilirsiniz. sliceState function’ınını daha generic hale getirip reducer’da karmaşıklığı giderebilirsiniz. Ancak şu an odak noktamız bu değil. Devam edelim…
PersonList component’ımıza id input’unu tanımlayıp action’larımıza ve selector’lerimize bu id’yi identifier olarak veriyoruz.
Selector’lere parametre gönderme konusunu daha detaylı öğrenmek isteyenler ‘ngrx parameterized selectors’ keyword’ü ile arama yapabilirler.
Effect’lerimizde maplediğimiz action’lara da tetikleyici action’dan identifier’ı alarak gönderiyoruz. Örnek olarak addPerson effect’ini ekliyorum. Diğer tüm effect’leri de aynı şekilde güncelledim.
Page’lerde component’larımıza id input’umuzu tanımlıyoruz.
Son olarak state’i component hazır olduğunda init edebilmek için initialize action’ı yaratıp component’ın ngOnInit hook’unda herşeyden önce dispatch ediyoruz. Reducer tarafında ise PersonList initial state’ini kapsayıcı state’imize yerleştiriyoruz.
Component tarafında initialize action’ını ilk iş olarak dispatch etmek önemli. Çünkü diğer durumda selector’lerimiz state undefined olduğu için hata verecektir. Bunun önüne geçiyoruz.
Vee taa daaa… Sonuç tam da istediğimiz gibi görünüyor :) Global state’i component’ımıza özel local state olarak kullanmayı başardık.
Hatta buradan page 2'ye geçtiğinizde ise destroy action’ı sayesinde state’lerin silindiğini bile görebilirsiniz. Stackblitz örneğini aşağıya ekliyorum. Konuyu daha net anlamanız için detaylı incelemenizi tavsiye ederim.
Sonuç
Bu yazı ile aslında amaçladığım şey soruna çözüm bulmak yerine karşılaştığımız problemin ne olduğunu net anlamanızı istedim. Zira konumuz daha bitmedi ancak bu yazı için sonuna geldik. E daha ne var işte herşey tamam diye düşünen arkadaşlar için kötü haber: Sorunlar bitmedi daha yapacak çok şey var. Serinin 2. yazısında yeni sorunlar ile devam edeceğiz.
Eğer global state’i local state olarak kullanmak istiyorsanız yukarıdaki yöntemi sakın kullanmayın :) Evde denemeyin gibi production’da denemeyin diyorum ve serinin 2. yazısında görüşmek üzere diyorum.
Hepinize iyi günler.