Angular’da Redux Tarzı Durum Yönetimi için ngrx ile Başlangıç
Angular, büyük ve karmaşık kurumsal uygulamalar geliştirmek için güçlü bir çerçevedir. Ancak, uygulama büyüdükçe ve bileşenler arası iletişim karmaşıklaştıkça, durum yönetimi (state management) önemli bir zorluk haline gelebilir. Verilerin farklı bileşenler arasında senkronize edilmesi, güncellenmesi ve tutarlı bir şekilde korunması, özellikle çok sayıda bileşenin aynı verilere erişmesi veya bu verileri değiştirmesi gerektiğinde, geliştiriciler için baş ağrısı yaratabilir. İşte bu noktada, Redux mimarisi ve onun Angular için adaptasyonu olan ngrx devreye girer.
Bu makalede, ngrx’in ne olduğunu, neden kullanılması gerektiğini, temel prensiplerini ve Angular uygulamalarınızda Redux tarzı durum yönetimini nasıl uygulayacağınızı adım adım inceleyeceğiz. Basit bir sayaç uygulaması örneği üzerinden ngrx’in ana bileşenleri olan Store, Actions, Reducers, Selectors ve Effects’i derinlemesine ele alacağız. Amacımız, ngrx’e yeni başlayanlar için kapsamlı bir rehber sunmak ve bu güçlü kütüphaneyi Angular projelerinize entegre etme konusunda size yol göstermektir.
Durum Yönetimi Neden Önemlidir?
Modern tek sayfa uygulamalarında (SPA’lar), kullanıcı arayüzü ve arkasındaki veri arasında sürekli bir etkileşim vardır. Bir kullanıcı bir düğmeye tıkladığında, bir form gönderdiğinde veya bir liste öğesi seçtiğinde, uygulamanın durumu değişir. Bu değişiklikler, uygulamanın farklı bölümlerinde yansıtılmalı ve tutarlılık sağlanmalıdır.
Geleneksel olarak, Angular’da durum yönetimi için @Input() ve @Output() dekoratörleri aracılığıyla bileşenler arası iletişim, servisler aracılığıyla veri paylaşımı veya RxJS Subject’ler kullanılmıştır. Ancak, uygulama hiyerarşisi derinleştikçe ve bileşenler arası bağımlılıklar arttıkça:
* Veri Akışı Karmaşıklaşır: Hangi verinin nerede güncellendiğini takip etmek zorlaşır.
* Hata Ayıklama Zorlaşır: Bir hatanın kaynağını bulmak, durumun hangi noktada tutarsız hale geldiğini anlamak güçleşir.
* Performans Sorunları Ortaya Çıkabilir: Gereksiz güncellemeler veya veri kopyalamaları performansı etkileyebilir.
* Kod Tekrarı Artar: Aynı veriyi yönetmek için farklı bileşenlerde benzer kodlar yazılabilir.
Bu sorunları çözmek için Redux gibi öngörülebilir durum yönetimi yaklaşımları geliştirilmiştir.
Redux Prensipleri ve ngrx’in Yeri
Redux, JavaScript uygulamaları için tahmin edilebilir durum kapsayıcısıdır. Temelde üç ana prensibe dayanır:
1. Tek Doğruluk Kaynağı (Single Source of Truth): Uygulamanızın tüm durumu tek bir büyük JavaScript nesnesinde saklanır. Bu nesneye “Store” adı verilir.
2. Durum Salt Okunurdur (State is Read-Only): Durum doğrudan değiştirilemez. Durumu değiştirmek için tek yol, bir “Action” adı verilen olayı göndermektir. Bu, durum değişikliklerinin izlenebilir ve öngörülebilir olmasını sağlar.
3. Değişiklikler Saf Fonksiyonlarla Yapılır (Changes are Made with Pure Functions): Durumu değiştiren fonksiyonlara “Reducers” denir. Reducers, mevcut durumu ve bir action’ı girdi olarak alır ve yeni bir durumu çıktı olarak döndürür. Yan etkilere (side effects) neden olmazlar ve her zaman aynı girdi için aynı çıktıyı üretirler.
ngrx, Redux prensiplerini Angular ekosistemine taşıyan bir kütüphane koleksiyonudur. RxJS’in gücünden yararlanarak, Angular uygulamalarında reaktif ve tahmin edilebilir bir durum yönetimi çözümü sunar. ngrx, sadece Store modülünden ibaret değildir; aynı zamanda Effects (yan etkileri yönetmek için), Selectors (durumdan veri seçmek için), Entity (koleksiyonları yönetmek için) ve Schematics (boilerplate kodunu azaltmak için) gibi ek modülleri de içerir.
ngrx Mimarisi ve Bileşenleri
ngrx, belirli bir veri akışı ve bileşenler arası etkileşim modeli sunar. Temel bileşenleri şunlardır:
* Store: Uygulamanın durumunu tutan merkezi depodur. Bileşenler bu depodan durumu okur ve action’lar göndererek durumu değiştirir.
* Actions: Uygulamada meydana gelen benzersiz olayları tanımlayan basit nesnelerdir. Örneğin, bir kullanıcının “ürün ekle” düğmesine tıklaması bir action’ı tetikleyebilir. Action’lar genellikle bir type özelliğine ve isteğe bağlı olarak bir payload (action ile birlikte taşınan veri) özelliğine sahiptir.
* Reducers: Action’ları dinleyen ve uygulamanın durumunu değiştiren saf fonksiyonlardır. Mevcut durumu ve tetiklenen action’ı girdi olarak alır, yeni bir durum nesnesi döndürür. Reducers asla mevcut durumu doğrudan mutasyona uğratmaz; her zaman yeni bir durum nesnesi döndürür.
* Selectors: Store’daki durumdan belirli veri parçalarını seçmek için kullanılan saf fonksiyonlardır. Performans optimizasyonu için memoization (belleğe alma) özelliklidirler, yani aynı girdi için daha önce hesaplanmış sonucu döndürürler.
* Effects: Uygulamanın dış dünyayla etkileşimini (API çağrıları, router navigasyonları, veritabanı işlemleri vb.) yöneten yan etkilerdir. Reducers’ın saf kalmasını sağlarlar ve asenkron işlemleri yönetmek için RxJS operatörlerini kullanırlar.
* Schematics: Angular CLI ile entegre olarak ngrx kod iskeletini otomatik olarak oluşturmaya yarayan araçlardır. Bu sayede boilerplate kodu azaltılır ve geliştirme hızı artırılır.
ngrx Kurulumu ve Temel Kullanım
ngrx’i kullanmaya başlamak için öncelikle bir Angular projesine ihtiyacımız var.
Proje Oluşturma
Eğer henüz bir Angular projeniz yoksa, Angular CLI kullanarak yeni bir proje oluşturabilirsiniz:
ng new ngrx-counter-app --routing false --style css
cd ngrx-counter-app
ngrx Kurulumu
ngrx paketlerini projenize eklemek için @ngrx/schematics kullanabilirsiniz. Bu, gerekli bağımlılıkları yükleyecek ve StoreModule‘ü uygulamanızın kök modülüne (app.module.ts) otomatik olarak yapılandıracaktır.
ng add @ngrx/store
Bu komut, @ngrx/store paketini kuracak ve app.module.ts dosyanızı aşağıdaki gibi güncelleyecektir:
// src/app/app.module.ts
import { NgModule, isDevMode } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { StoreModule } from '@ngrx/store'; // ngrx/store eklendi
import { StoreDevtoolsModule } from '@ngrx/store-devtools'; // ngrx/store-devtools eklendi
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
StoreModule.forRoot({}), // Kök durum nesnesi tanımlandı
StoreDevtoolsModule.instrument({ maxAge: 25, logOnly: !isDevMode() }) // Geliştirici araçları
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
StoreModule.forRoot({}) ifadesi, uygulamanızın kök durumunu tanımlar. Şimdilik boş bir nesne olarak bırakabiliriz. StoreDevtoolsModule ise Redux DevTools eklentisi ile entegrasyonu sağlar, bu da durum değişikliklerini izlemek ve hata ayıklamak için çok kullanışlıdır.
Adım Adım ngrx Uygulaması Oluşturma: Basit Bir Sayaç
Şimdi, ngrx’in temel bileşenlerini kullanarak basit bir sayaç uygulaması geliştirelim. Bu sayaç, artırma, azaltma ve sıfırlama işlevlerine sahip olacak.
1. State Tanımlama
Öncelikle, uygulamanızın durumunu (state) tanımlamamız gerekiyor. Basit bir sayaç için sadece bir number değeri yeterli olacaktır. Genellikle durumunuzu bir arayüz (interface) ile tanımlamak iyi bir pratiktir.
src/app/state/app.state.ts (veya src/app/counter/counter.state.ts gibi daha modüler bir yapı)
export interface AppState {
counter: number;
}
export const initialState: AppState = {
counter: 0
};
Burada AppState arayüzü, uygulamamızın kök durumunun nasıl görüneceğini belirtir. initialState ise uygulamanın başlangıç durumunu tanımlar.
2. Actions Tanımlama
Action’lar, uygulamamızdaki olayları temsil eder. Sayaç uygulamamız için “artır”, “azalt” ve “sıfırla” olmak üzere üç action’a ihtiyacımız var. ngrx’in @ngrx/store paketinden createAction ve props fonksiyonlarını kullanarak action’ları tanımlayabiliriz.
src/app/state/counter.actions.ts
import { createAction, props } from '@ngrx/store';
// [Counter] Ön eki, bu action'ın hangi alana ait olduğunu gösterir.
// Bu bir isimlendirme kuralıdır ve okunabilirliği artırır.
export const increment = createAction('[Counter] Increment');
export const decrement = createAction('[Counter] Decrement');
export const reset = createAction(
'[Counter] Reset',
props() // Bu action bir payload (yük) taşıyacak
);
// Effects için de action'lar tanımlayabiliriz (ileride kullanacağız)
export const loadCounter = createAction('[Counter] Load Counter');
export const loadCounterSuccess = createAction(
'[Counter] Load Counter Success',
props()
);
export const loadCounterFailure = createAction(
'[Counter] Load Counter Failure',
props()
);
createAction fonksiyonu, benzersiz bir tür dizesi alır. props fonksiyonu ise action ile birlikte taşınacak ek verileri (payload) tanımlamamızı sağlar. reset action’ı bir value payload’ı alarak sayacı belirli bir değere sıfırlayacaktır.
3. Reducers Oluşturma
Reducers, action’ları dinleyen ve duruma yeni bir değer atayan saf fonksiyonlardır. @ngrx/store paketinden createReducer ve on fonksiyonlarını kullanarak reducer’ımızı oluşturabiliriz.
src/app/state/counter.reducer.ts
import { createReducer, on } from '@ngrx/store';
import { increment, decrement, reset, loadCounterSuccess } from './counter.actions';
import { initialState } from './app.state'; // Başlangıç durumunu import ediyoruz
export const counterReducer = createReducer(
initialState, // Reducer'ın başlangıç durumu
on(increment, (state) => ({ ...state, counter: state.counter + 1 })), // Artırma action'ı
on(decrement, (state) => ({ ...state, counter: state.counter - 1 })), // Azaltma action'ı
on(reset, (state, { value }) => ({ ...state, counter: value })), // Sıfırlama action'ı (payload ile)
on(loadCounterSuccess, (state, { data }) => ({ ...state, counter: data })) // Async yükleme başarılı
);
* createReducer fonksiyonu, başlangıç durumunu ve bir dizi on fonksiyonunu alır.
* on fonksiyonu, belirli bir action tetiklendiğinde çalışacak bir geri çağırma (callback) fonksiyonu tanımlar.
* Bu geri çağırma fonksiyonu, mevcut durumu (state) ve action’ın kendisini (eğer payload varsa) alır.
* Önemli: Reducer fonksiyonları, mevcut durumu doğrudan değiştirmemeli (immutable olmalı). Bunun yerine, mevcut durumun bir kopyasını almalı ve sadece değişen özellikleri güncellemelidir. Spread operatörü (...state) bu amaçla kullanılır.
4. Store’u Uygulamaya Entegre Etme
Şimdi reducer’ımızı app.module.ts dosyasındaki StoreModule.forRoot() metoduna eklememiz gerekiyor.
// src/app/app.module.ts
import { NgModule, isDevMode } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { StoreModule } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { AppComponent } from './app.component';
import { counterReducer } from './state/counter.reducer'; // Reducer'ımızı import ediyoruz
import { AppState } from './state/app.state'; // AppState arayüzünü import ediyoruz
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
StoreModule.forRoot({ // AppState tipini belirtiyoruz
counter: counterReducer // 'counter' anahtarı altında counterReducer'ı kaydediyoruz
}),
StoreDevtoolsModule.instrument({ maxAge: 25, logOnly: !isDevMode() })
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Burada StoreModule.forRoot({ counter: counterReducer }) ile global durumumuzun counter adlı bir özelliğe sahip olduğunu ve bu özelliğin counterReducer tarafından yönetildiğini belirtiyoruz. Artık Store servisimiz, AppState arayüzüne uygun bir yapıya sahip olacak.
5. Store’u Kullanma (Component Tarafı)
Artık bileşenlerimizden durumu okuyabilir ve action’lar gönderebiliriz. AppComponent‘i güncelleyelim.
src/app/app.component.ts
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { increment, decrement, reset, loadCounter } from './state/counter.actions';
import { AppState } from './state/app.state';
@Component({
selector: 'app-root',
template:
})
export class AppComponent {
counter$: Observable;
constructor(private store: Store) {
// Store'dan 'counter' durumunu seçiyoruz.
// 'select' metodu bir Observable döndürür.
this.counter$ = this.store.select(state => state.counter);
}
increment() {
this.store.dispatch(increment()); // increment action'ını gönder
}
decrement() {
this.store.dispatch(decrement()); // decrement action'ını gönder
}
reset(value: number) {
this.store.dispatch(reset({ value })); // reset action'ını payload ile gönder
}
loadCounter() {
this.store.dispatch(loadCounter()); // loadCounter action'ını gönder (Effects tarafından işlenecek)
}
}
* Store servisini enjekte ediyoruz.
* this.store.select(state => state.counter) ile AppState içindeki counter özelliğini seçiyoruz. Bu bir RxJS Observable döndürür, bu yüzden şablonumuzda async pipe ile abone oluyoruz.
* this.store.dispatch(action()) ile action’ları Store’a gönderiyoruz. Store, bu action’ları dinleyen reducer’ları tetikler.
6. Selectors Oluşturma
Selector’lar, durumdan belirli veri parçalarını seçmek ve türetilmiş durumlar oluşturmak için kullanılır. createSelector fonksiyonu ile memoized (belleğe alınmış) selector’lar oluşturabiliriz, bu da performans açısından önemlidir çünkü aynı girdi için hesaplama tekrarlanmaz.
src/app/state/counter.selectors.ts
import { createSelector, createFeatureSelector } from '@ngrx/store';
import { AppState } from './app.state';
// Kök durumdan 'counter' özelliğini seçen bir feature selector tanımlıyoruz.
// Bu, genellikle daha büyük uygulamalarda modül bazlı durumları seçmek için kullanılır.
export const selectCounterState = createFeatureSelector('counter');
// selectCounterState'i girdi olarak kullanan bir selector.
// Bu selector, 'counter' özelliğinin kendisini döndürür.
export const selectCounter = createSelector(
selectCounterState,
(state: AppState) => state.counter
);
// Türetilmiş bir durum örneği: sayacın çift mi tek mi olduğunu kontrol eden selector
export const selectIsEven = createSelector(
selectCounter,
(counter: number) => counter % 2 === 0
);
Şimdi AppComponent‘te bu selector’ları kullanabiliriz:
// src/app/app.component.ts'te değişiklikler
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { increment, decrement, reset, loadCounter } from './state/counter.actions';
import { AppState } from './state/app.state';
import { selectCounter, selectIsEven } from './state/counter.selectors'; // Selector'ları import et
@Component({
selector: 'app-root',
template:
Sayı {{ (isEven$ | async) ? 'çift' : 'tek' }}
})
export class AppComponent {
counter$: Observable;
isEven$: Observable; // Yeni Observable
constructor(private store: Store) {
this.counter$ = this.store.select(selectCounter); // Selector kullanarak seçiyoruz
this.isEven$ = this.store.select(selectIsEven); // Türetilmiş selector kullanıyoruz
}
// ... (diğer metodlar aynı kalır)
}
Selector’lar, bileşenlerin doğrudan durumun yapısını bilmesini engeller, bu da kodun daha modüler ve bakımı kolay olmasını sağlar. Ayrıca, createSelector‘ın memoization özelliği sayesinde, durum değişmediği sürece aynı selector’ın çıktısı yeniden hesaplanmaz, bu da performansı artırır.
ngrx/effects ile Asenkron İşlemler ve Yan Etkiler
Reducer’lar saf fonksiyonlar olmalı ve yan etkilere neden olmamalıdır. Peki ya API çağrıları, router navigasyonları veya veritabanı işlemleri gibi asenkron işlemler? İşte bu noktada ngrx/effects devreye girer. Effects, action’ları dinler, yan etkileri gerçekleştirir ve genellikle yeni action’lar göndererek Store’u günceller.
Effects Kurulumu
ngrx/effects paketini projenize ekleyin:
ng add @ngrx/effects
Bu komut EffectsModule‘ü app.module.ts‘e ekleyecektir.
// src/app/app.module.ts'te oluşan değişiklik
import { EffectsModule } from '@ngrx/effects';
@NgModule({
// ...
imports: [
// ...
EffectsModule.forRoot([]), // Boş bir dizi ile başlarız
// ...
],
// ...
})
export class AppModule { }
Effects Oluşturma
src/app/state/counter.effects.ts
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { of } from 'rxjs';
import { map, concatMap, catchError, delay } from 'rxjs/operators';
import { loadCounter, loadCounterSuccess, loadCounterFailure } from './counter.actions';
@Injectable()
export class CounterEffects {
constructor(private actions$: Actions) {}
loadCounter$ = createEffect(() =>
this.actions$.pipe(
ofType(loadCounter), // Sadece 'loadCounter' action'ını dinle
concatMap(() =>
// Gerçek bir API çağrısı yerine bir simülasyon yapıyoruz
of(Math.floor(Math.random() * 100)).pipe( // Rastgele bir sayı üret
delay(1000), // 1 saniye gecikme simülasyonu
map(data => loadCounterSuccess({ data })), // Başarılı olursa loadCounterSuccess action'ını gönder
catchError(error => of(loadCounterFailure({ error }))) // Hata olursa loadCounterFailure action'ını gönder
)
)
)
);
}
* @Injectable() dekoratörü, bu sınıfın bağımlılık enjeksiyonu için kullanılabilir olduğunu belirtir.
* Actions servisi, uygulamanın tüm action’larının bir Observable akışıdır.
* createEffect fonksiyonu, bir yan etkiyi tanımlar.
* this.actions$.pipe(): Actions akışı üzerinde RxJS operatörlerini kullanırız.
* ofType(loadCounter): Sadece loadCounter action’ı tetiklendiğinde sonraki operatörleri çalıştırmamızı sağlar.
* concatMap(): Önceki Observable tamamlandığında yeni bir Observable’ı birleştirmek için kullanılır. Bu, sıralı asenkron işlemleri yönetmek için iyidir.
of(Math.floor(Math.random() 100)).pipe(delay(1000)): Bir API çağrısını taklit eden bir RxJS Observable’ı oluşturur. 1 saniye sonra rastgele bir sayı yayar.
* map(data => loadCounterSuccess({ data })): Simülasyon başarılı olduğunda, loadCounterSuccess action’ını uygun payload ile dispatch ederiz.
* catchError(error => of(loadCounterFailure({ error }))): Eğer bir hata oluşursa, loadCounterFailure action’ını dispatch ederiz.
Effects Modülünü Yapılandırma
Oluşturduğumuz CounterEffects sınıfını app.module.ts dosyasındaki EffectsModule.forRoot() metoduna eklememiz gerekiyor.
// src/app/app.module.ts'te değişiklikler
import { EffectsModule } from '@ngrx/effects';
import { CounterEffects } from './state/counter.effects'; // Effects sınıfımızı import ediyoruz
@NgModule({
// ...
imports: [
// ...
EffectsModule.forRoot([CounterEffects]), // CounterEffects'i kaydediyoruz
// ...
],
// ...
})
export class AppModule { }
Artık AppComponent‘teki loadCounter() metodu tetiklendiğinde, CounterEffects‘teki loadCounter$ effect’i çalışacak, asenkron işlemi simüle edecek ve sonucuna göre loadCounterSuccess veya loadCounterFailure action’ını Store’a gönderecektir. counterReducer ise loadCounterSuccess action’ını dinleyerek sayacı güncelleyecektir.
ngrx/entity ile Koleksiyon Yönetimi
Çoğu uygulamada, kullanıcılar, ürünler, görevler gibi bir dizi öğeyi yönetmek gerekir. Bu tür koleksiyonları yönetmek, ekleme, güncelleme, silme ve sıralama gibi işlemleri içerir. ngrx/entity modülü, bu tür koleksiyonları yönetmek için optimize edilmiş reducer’lar ve selector’lar oluşturarak boilerplate kodunu azaltır ve performansı artırır.
ngrx/entity kullanmak, koleksiyonları normalleştirilmiş bir yapıda (ID’ye göre bir nesne ve tüm ID’lerin bir dizisi) tutmanızı sağlar, bu da değişiklikleri daha verimli hale getirir.
Örneğin, bir “Todo” listesi yönetmek için:
1. Entity Adapter Oluşturma:
import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity';
export interface Todo {
id: string;
title: string;
completed: boolean;
}
export interface TodoState extends EntityState {
// Ek durum özellikleri buraya eklenebilir
selectedTodoId: string | null;
}
export const todoAdapter: EntityAdapter = createEntityAdapter();
export const initialTodoState: TodoState = todoAdapter.getInitialState({
selectedTodoId: null
});
2. Reducer’da Kullanma:
import { createReducer, on } from '@ngrx/store';
import { todoAdapter, initialTodoState } from './todo.state';
import * as TodoActions from './todo.actions';
export const todoReducer = createReducer(
initialTodoState,
on(TodoActions.addTodo, (state, { todo }) => {
return todoAdapter.addOne(todo, state);
}),
on(TodoActions.updateTodo, (state, { update }) => {
return todoAdapter.updateOne(update, state);
}),
on(TodoActions.deleteTodo, (state, { id }) => {
return todoAdapter.removeOne(id, state);
}),
on(TodoActions.loadTodosSuccess, (state, { todos }) => {
return todoAdapter.setAll(todos, state);
})
// ... diğer action'lar
);
3. Selector’larda Kullanma:
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { TodoState, todoAdapter } from './todo.state';
export const selectTodoState = createFeatureSelector('todos');
export const {
selectIds,
selectEntities,
selectAll,
selectTotal,
} = todoAdapter.getSelectors(selectTodoState);
export const selectSelectedTodoId = createSelector(
selectTodoState,
(state: TodoState) => state.selectedTodoId
);
export const selectSelectedTodo = createSelector(
selectEntities,
selectSelectedTodoId,
(entities, selectedId) => selectedId && entities[selectedId]
);
ngrx/entity, özellikle büyük ve sık güncellenen koleksiyonlar için kodunuzu önemli ölçüde basitleştirir ve performansı artırır.
ngrx Geliştirici Araçları (DevTools)
ngrx’in en büyük avantajlarından biri, Redux DevTools ile olan entegrasyonudur. StoreDevtoolsModule sayesinde, tarayıcınızdaki Redux DevTools eklentisini kullanarak uygulamanızın durumunu ve tüm action geçmişini izleyebilirsiniz. Bu, hata ayıklama sürecini inanılmaz derecede kolaylaştırır.
app.module.ts dosyasında StoreDevtoolsModule.instrument() ile zaten kurmuştuk. Geliştirme ortamınızda bu araçları kullanarak:
* Durum Geçmişini Görüntüleme: Uygulamanın durumunun her bir action’dan sonra nasıl değiştiğini adım adım görebilirsiniz.
* Time-Travel Debugging: Geçmişteki bir duruma geri dönüp uygulamanın o anki halini inceleyebilirsiniz.
* Action’ları İnceleme: Hangi action’ların ne zaman ve hangi payload ile gönderildiğini görebilirsiniz.
* Durumu Düzenleme: Anlık olarak durumu değiştirerek farklı senaryoları test edebilirsiniz.
Bu araçlar, ngrx ile çalışırken geliştirici deneyimini büyük ölçüde iyileştirir ve karmaşık durum sorunlarını hızla teşhis etmenize yardımcı olur.
ngrx’in Avantajları ve Dezavantajları
Her teknoloji gibi, ngrx’in de kendine göre avantajları ve dezavantajları vardır.
Avantajları:
* Merkezi Durum Yönetimi: Uygulamanın tüm durumu tek bir yerde tutulur, bu da veri akışını öngörülebilir hale getirir.
* Tahmin Edilebilir Durum: Durum değişiklikleri action’lar ve saf reducer’lar aracılığıyla gerçekleştiği için, durumun ne zaman ve nasıl değiştiği her zaman bellidir.
* Geliştirici Araçları: Redux DevTools ile mükemmel entegrasyon, hata ayıklamayı ve durum geçmişini izlemeyi kolaylaştırır.
* Büyük Ölçekli Uygulamalar İçin Uygunluk: Karmaşık ve büyük ölçekli uygulamalarda durum karmaşasını yönetmek için idealdir.
* Performans Optimizasyonu: createSelector ile memoized selector’lar, gereksiz yeniden hesaplamaları önleyerek performansı artırır.
* Test Edilebilirlik: Reducer’lar ve selector’lar saf fonksiyonlar olduğu için kolayca test edilebilirler. Effects’ler de izole bir şekilde test edilebilir.
* Modülerlik: Uygulamanın farklı katmanları (UI, iş mantığı, yan etkiler) net bir şekilde ayrılmıştır.
Dezavantajları:
* Öğrenme Eğrisi: Redux prensiplerini ve RxJS operatörlerini anlamak zaman alabilir. Yeni başlayanlar için başlangıçta göz korkutucu olabilir.
* Boilerplate Kodu: Özellikle küçük uygulamalar için, çok sayıda dosya (actions, reducers, effects, selectors) ve tekrar eden kod (boilerplate) oluşturma ihtiyacı doğurabilir. Ancak ngrx schematics ve yeni API’ler (createAction, createReducer, createEffect) bu sorunu önemli ölçüde azaltmıştır.
* Küçük Uygulamalar İçin Gereksiz Karmaşıklık: Eğer uygulamanızın durumu basitse ve az sayıda bileşen arasında paylaşılıyorsa, ngrx kullanmak gereksiz karmaşıklık getirebilir.
Ne Zaman ngrx Kullanmalı?
ngrx her Angular uygulaması için doğru çözüm değildir. Aşağıdaki senaryolarda ngrx kullanmayı düşünebilirsiniz:
* Uygulamanızın Durumu Karmaşıklaştığında: Birçok farklı veri parçasının birbiriyle ilişkili olduğu ve birçok bileşen tarafından kullanıldığı durumlarda.
* Birçok Bileşenin Aynı Duruma Erişmesi veya Durumu Değiştirmesi Gerektiğinde: Bileşen ağacının farklı yerlerindeki bileşenlerin aynı veriler üzerinde çalışması gerektiğinde.
* Asenkron İşlemlerin Yoğun Olduğu Durumlarda: API çağrıları, web soketleri veya diğer asenkron veri akışlarının çok olduğu uygulamalarda Effects modülü çok faydalıdır.
* Durum Değişikliklerinin İzlenebilir ve Tahmin Edilebilir Olması Gerektiğinde: Uygulamanızda bir hata oluştuğunda, durumun hangi noktada ve hangi action ile değiştiğini kolayca tespit etmek istediğinizde.
* Ekip Üyelerinin Redux Prensiplerine Aşina Olduğu Durumlarda: Ekibiniz zaten Redux veya benzeri bir desenle deneyimliyse, ngrx’e geçiş daha kolay olacaktır.
Basit CRUD uygulamaları veya az sayıda bileşeni olan küçük uygulamalar için, servisler ve RxJS Subject‘ler gibi daha hafif çözümler yeterli olabilir. ngrx’in getirdiği öğrenme maliyeti ve boilerplate kodunu göz önünde bulundurarak, uygulamanızın ihtiyaçlarına göre bilinçli bir karar vermek önemlidir.
Sonuç
ngrx, Angular uygulamalarında Redux tarzı durum yönetimini uygulamak için güçlü ve kapsamlı bir çözümdür. Uygulamanız büyüdükçe ortaya çıkan durum yönetimi karmaşıklıklarını çözmek, veri akışını tahmin edilebilir kılmak ve hata ayıklamayı kolaylaştırmak için merkezi bir yaklaşım sunar. Store, Actions, Reducers, Selectors ve Effects gibi temel bileşenleriyle, uygulamanızın durumunu, iş mantığını ve yan etkilerini düzenli bir şekilde ayırmanıza olanak tanır.
Başlangıçta bir öğrenme eğrisi olsa da, ngrx’e yapılan yatırım, özellikle büyük ve karmaşık Angular projelerinde, daha sürdürülebilir, test edilebilir ve yönetilebilir bir kod tabanı ile geri dönecektir. Geliştirici araçları sayesinde, uygulamanızın durumunu izlemek ve hata ayıklamak hiç bu kadar kolay olmamıştı.
Bu makalede sunduğumuz basit sayaç örneği, ngrx’in temel prensiplerini ve nasıl çalıştığını anlamanız için bir başlangıç noktasıdır. ngrx ekosistemi oldukça geniştir ve keşfedilmeyi bekleyen daha birçok güçlü özelliği (router-store, component-store, data, vb.) bulunmaktadır. Kendi projelerinizde ngrx’i deneyerek ve topluluk kaynaklarını takip ederek bu güçlü kütüphaneyi tam potansiyeliyle kullanabilirsiniz. Unutmayın, doğru araçları doğru zamanda seçmek, başarılı bir yazılım geliştirme sürecinin anahtarıdır.
