NGRX - Component’a Özel State Kullanmak — 1

Global state’i local state olarak kullanmak ve global state’i dilimlemek

Emre Hızlı
7 min readJan 30, 2021

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.

Component’ımızın son hali

Ö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:

Folder yapısı

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.

destroy action
destroy reducer
component ngOnDestroy

Çö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…

selectors

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.

component

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.

effect

Page’lerde component’larımıza id input’umuzu tanımlıyoruz.

page

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.

initialize action
initialize reducer
component ngOnInit

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.

--

--