Angular Signals ve Rxjs Farkları Nelerdir?

Nahit Ferhat Ektaş
7 min readAug 31, 2024

--

Angular 16 öncesinde Signals ile yapabildiğimiz işlemlerin bir çoğunu Rxjs kütüphanesinden yararlanarak gerçekleştiriyorduk.

Peki o zaman Signals, Rxjs kütüphanesinin yerini aldı diyebilir miyiz?

Aslında Signals ilk tanıtıldığında sanki öyle bir durum varmış gibi algılandı ama aslında tam olarak öyle değil.

Rxjs asenkron işlemler için harika bir kütüphane olsa da, senkron işlemler için oldukça zorlayıcıydı.

Bunu örnek üzerinden açıklayacak olursak biraz daha anlaşılır olacak

@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet],
template: `
<div class="relative overflow-x-auto p-5">
<div
class="max-w-sm p-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700"
>
<div class="border-b-orange-400 border-b py-1 px-5">
<div class="flex items-center justify-between">
<h5 class="font-thin">Price:</h5>
<p>{{ price$.getValue() }} $</p>
</div>
</div>

<div class="border-b-orange-400 border-b py-1 px-5">
<div class="flex items-center justify-between">
<h5 class="font-thin">Quantity:</h5>
<p>{{ quantity$.getValue() }} $</p>
</div>
</div>

<div class="border-b-orange-400 border-b py-1 px-5">
<div class="flex items-center justify-between">
<h5 class="font-thin">Discount:</h5>
<p>{{ discount$.getValue() }} %</p>
</div>
</div>

<div class="py-1 px-5">
<div class="flex items-center justify-between">
<h5 class="font-thin">Total Price:</h5>
<p class="text-orange-400 font-semibold">
{{ totalPrice$ | async }} $
</p>
</div>
</div>

<button
(click)="calculate()"
class="inline-flex mt-5 px-5 w-full py-2 text-sm font-medium justify-center text-white bg-orange-400 rounded-lg hover:bg-orange-500 focus:ring-4 focus:outline-none"
>
Calculate
</button>
<p>{{ counter }}</p>
</div>
</div>
`,
styleUrl: './app.component.css',
})
export class AppComponent implements OnInit {
counter = 0;
price$ = new BehaviorSubject(10);
quantity$ = new BehaviorSubject(4);
discount$ = new BehaviorSubject(50);
totalPrice$ = combineLatest([
this.price$,
this.quantity$,
this.discount$,
]).pipe(
tap(() => this.counter++),
map(([price, quantity, discount]) => {
const total = price * quantity;
return total - (total * discount) / 100;
})
);

ngOnInit(): void {
initFlowbite();
}

calculate() {
this.price$.next(120);
this.quantity$.next(5);
this.discount$.next(30);
}
}

Yukarıdaki örnekte, price, quantity ve discount adında 3 tane behaviorSubject tanımladık. Bu 3 değerin değişimine göre totalPrice değerini yeniden hesaplayabilmek için Rxjs kütüphanesinde yer alan combineLatest operatöründen yararlandık. Button’a her tıklanıldığında calculate() fonksiyonu çalışacak ve price, quantity ve discount değerlerini güncelleyecek.

Diğer taraftan button’a her basıldığında, işlemin kaç kere tekrar ettiğini takip etmek için counter adında bir değişken oluşturduk ve tap operatörü ile akış her yenilendiğinde değeri arttırdık. Başlangıç olarak 0 tanımlanmış olsada sayfa ilk render edildiğinde başlangıç değerleri ile çalıştığı için değer 1 olarak gösterildi.

Button’ a tıkladığımızda tüm değerler değiştiği için, total price yeni değerlere göre yeniden hesaplandı. Ancak counter değişkenimizin 2 olmasını beklerken 4 oldu.

Bunun nedeni, başlangıçta tanımladığımız üç değişkenin(price, quantity ve discount) aynı anda güncellenmesinden meydana geliyor. combineLatest operatörü akışa gelen değerler değiştiği için her değişiklik için hesaplamayı tekrardan gerçekleştiriyor.

Bunun önüne geçebilmek için yine rxjs kütüphanesinde yer alan debounceTime operatöründen yararlanıyoruz.

totalPrice$ = combineLatest([
this.price$,
this.quantity$,
this.discount$,
]).pipe(
debounceTime(0),
tap(() => this.counter++),
map(([price, quantity, discount]) => {
const total = price * quantity;
return total - (total * discount) / 100;
})
);

DebounceTime operatörü ile akıştan gelen değerler arasına zaman ekleyebiliyoruz. Bu sayede 3 değerinde aynı anda akışa girmesini engellemiş oluyoruz.

Bunu sorunu aştığımıza göre bir diğer soruna geçebiliriz. Yukarıdaki ekranda counter değerinin 4 yerine 2 olduğunu gördük ama button’a tekrar tıkladığımızda hiçbir değişkenin değeri değişmemesine rağmen akışın tekrar çalıştığını ve counter değerinin yine arttığını görüyoruz. Eğer değerler değişmemiş ise sayfanın yeniden render edilmesi de anlamsız olacak.

Bunun içinde yine Rxjs kütüphanesinde yer alan distinctUntilChanged() operatöründen yararlanıyoruz.

totalPrice$ = combineLatest([
this.price$.pipe(distinctUntilChanged()),
this.quantity$.pipe(distinctUntilChanged()),
this.discount$.pipe(distinctUntilChanged()),
]).pipe(
debounceTime(0),
tap(() => this.counter++),
map(([price, quantity, discount]) => {
const total = price * quantity;
return total - (total * discount) / 100;
})
);

DistrinctUntilChanged operatörü ile değer değişene kadar akışa dahil etme demiş oluyoruz ve değerler değişmediği için counter değerimiz sabit kalıyor.

Peki son olarak totalPrice$ değerini başka bir component’de veya sayfamızın başka bir yerinde tekrar kullanmak istediğimizi hayal edelim.

Gönderilen değer aynı olsa bile aynı akış 2 kere çalışıyor. Bunun önüne geçmek için ise shareReplay operatöründen yararlanıyoruz.

totalPrice$ = combineLatest([
this.price$.pipe(distinctUntilChanged()),
this.quantity$.pipe(distinctUntilChanged()),
this.discount$.pipe(distinctUntilChanged()),
]).pipe(
debounceTime(0),
tap(() => this.counter++),
map(([price, quantity, discount]) => {
const total = price * quantity;
return total - (total * discount) / 100;
}),
shareReplay()
);

ShareReplay operatörü ile totalPrice$ değerini tüketen tüm alanlar için tek bir veri gönderilmesini sağlıyoruz. Tabi burda shareReplay operatörünü subscription’dan önce yazmamız önemli yoksa çalışmayacaktır.

Yukarıdaki örnekte olduğu gibi, yapmak istediğimiz işlem çok sıradan olsa da çok fazla rxjs operatörüne maruz kaldık ve çok fazla kod yazdık. Aynı işlemi rxjs olmadan Signals kullanarak yaptığımızda

@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet],
template: `
<div class="relative overflow-x-auto p-5">
<div
class="max-w-sm p-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700"
>
<div class="border-b-orange-400 border-b py-1 px-5">
<div class="flex items-center justify-between">
<h5 class="font-thin">Price:</h5>
<p>{{ price() }} $</p>
</div>
</div>

<div class="border-b-orange-400 border-b py-1 px-5">
<div class="flex items-center justify-between">
<h5 class="font-thin">Quantity:</h5>
<p>{{ quantity() }} $</p>
</div>
</div>

<div class="border-b-orange-400 border-b py-1 px-5">
<div class="flex items-center justify-between">
<h5 class="font-thin">Discount:</h5>
<p>{{ discount() }} $</p>
</div>
</div>

<div class="py-1 px-5">
<div class="flex items-center justify-between">
<h5 class="font-thin">Total Price:</h5>
<p class="text-orange-400 font-semibold">{{ totalPrice() }} $</p>
</div>
</div>
<div class="py-1 px-5">
<div class="flex items-center justify-between">
<h5 class="font-thin">Total Price:</h5>
<p class="text-orange-400 font-semibold">{{ totalPrice() }} $</p>
</div>
</div>
<button
(click)="calculate()"
class="inline-flex mt-5 px-5 w-full py-2 text-sm font-medium justify-center text-white bg-orange-400 rounded-lg hover:bg-orange-500 focus:ring-4 focus:outline-none"
>
Calculate
</button>
<p>{{ counter }}</p>
</div>
</div>
`,
styleUrl: './app.component.css',
})
export class AppComponent implements OnInit {
counter = 0;
price = signal(10);
quantity = signal(4);
discount = signal(50);
totalPrice = computed(() => {
const total = this.price() * this.quantity();
this.counter++;
return total - (total * this.discount()) / 100;
});

ngOnInit(): void {
initFlowbite();
}

calculate() {
this.price.set(10);
this.quantity.set(5);
this.discount.set(30);
}
}

Rxjs tarafında operatörler yardımı ile gerçekleştirdiğimiz karmaşık işlemleri, Signals kullanarak kolaylıkla gerçekleştirebiliyoruz.

  • Signals kendi değeri değişmediği sürece sayfamızı yeniden render etmez(distrinctUntilChange gibi bir operatör gerekli değildir)
  • Signals bağımlı olduğu diğer signal’ların eş zamanlı değişimine ayak uydurur ve son signal değiştiğinde kendini yeniler.(debounceTime gibi bir operatör gerekli değildir)
  • Signals kendini tüketen componentler için birden fazla kez çalışmaz, bir kez çalışır ve aynı değeri gönderir(shareReplay gibi bir operatör gerekli değildir)

O zaman yazının başındaki sorumuza geri dönelim. Signals, Rxjs’in yerini mi aldı? Cevap hala hayır!

Şimdi gelin Rxjs’in Signals’dan daha kolay olduğu durumlara bakalım.

Rxjs farklı kaynaklardan gelen event’leri yönetmekte oldukça başarılı bir kütüphanedir. Zaten kullanım amacıda aslında event yönetimi içindir. İçerisinde yer alan bir çok operatör yardımı ile event’ler tetiklendiğinde bu event’lere erişip çeşitli aksiyonlar almamıza yardımcı olur. Aynı işlem Signals ile de gerçekleştirilebilir, ancak Rxjs kadar verimli değildir. Rxjs, event’leri Signals’ a göre daha kolay ve daha okunabilir kodlama ile yönetmemizi sağlar.

Örnek üzerinden ilerleyecek olursak,

@Component({
selector: 'app-root',
standalone: true,
template: `
<input type="text" [formControl]="searchControl" placeholder="Search">
`,
imports: [ReactiveFormsModule],
})
export class App {
searchControl = new FormControl('');

constructor(){
this.searchControl.valueChanges
.pipe(
debounceTime(300),
)
.subscribe(searchTerm => {
// Perform desired action or API call with the debounced and distinct search term
console.log(searchTerm);
});
}
}

Basit bir input’umuz var ve kullanıcı değer girdiğinde, girilen değeri karşılayıp bir işlem gerçekleştiriyorum. Yukarıdaki örnekte console.log() yaptık ama uzak bir API’ye istek attığımızı varsayalım. Kullanıcı her harf girdiğinde servise istek atmak yerine araya biraz zaman koyup örneğin 3–4 harf girdikten sonra isteği gerçekleştirmek istediğim durumda debounceTime() operatörünü kullanarak bu işlemi kolaylıkla gerçekleştirebiliyorum.

Şimdi aynı işlemi Signals kullanarak gerçekleştirelim.

@Component({
selector: 'app-root',
standalone: true,
template: `
<input type="text" [ngModel]="searchValue()" (ngModelChange)="searchValue.set($event)" placeholder="Search">
`,
imports: [ReactiveFormsModule, FormsModule],
})
export class App {
searchValue = signal<any>('');
debouncedSearchValue = signal(this.searchValue());

constructor() {
let timeoutId: ReturnType<typeof setTimeout> | undefined;
effect((onCleanup) => {
const search = this.searchValue();

timeoutId = setTimeout(() => {
this.debouncedSearchValue.set(search);
}, 500);

onCleanup(() => {
clearTimeout(timeoutId);
});
});

effect(() => {
console.log(this.debouncedSearchValue());
});
}
}

Yukarıdaki kodu inceleyecek olursak, searchValue her değiştiğinde effect yeniden tetiklendi. timeoutId değişkeninde gelen değeri bekletip debouncedSearchValue değişkenine set edip timeoutId’yi temizledik, debouncedSearchValue değişkenine yeni değer gelincede en alttaki effect çalıştı ve console.log() yaparak değeri yazdırdık.

Görüldüğü gibi, Rxjs kullanarak daha kolay ve daha okunabilir olarak yaptığımız işlemi Signals kullarak daha karışık bir hale getirdik.

Rxjs bize asenkron işlemlerde oldukça yarar sağlarken senkron işlemlerde oldukça zorlayıcı olabiliyordu. Signals ise, verinin senkron olarak yönetilmesi gereken süreçlerde ciddi avantaj sağladı. Unutmamak gerekir ki iki yaklaşım ile de kodlama yapılabilir ama iki teknolojinin de sağladığı yararlar birbirinden farklıdır.

--

--