Mert Özen Her Satırda Daha İleri
Frontend RxJS — #03

Marble Diagram Okumak ve Temel Operator'ler: map, filter, scan, take, distinctUntilChanged

Mert Özen 14 Kas 2026 14 dk 12 görüntülenme
Marble Diagram Okumak ve Temel Operator'ler: map, filter, scan, take, distinctUntilChanged

Marble diagram bir Observable'ın zaman içindeki değerlerini, complete'ini ve hatasını tek satırda gösterir; pipeable operator ise Observable alıp yeni bir Observable döndüren bir fonksiyondur. map, scan, reduce, pairwise, filter, takeWhile, skip, first, last ve distinctUntilChanged'i TestScheduler ile doğrulanmış marble'larla inceliyor; EmptyError ve object karşılaştırma tuzaklarını Bookstore sepeti ve katalog filtresiyle gösteriyoruz.

Sürüm notu: Bu yazıdaki örnekler RxJS 7.8.2, TypeScript 6.0 ve Node.js 24 ile denenmiştir; marble diagram'lar RxJS'in TestScheduler'ı ile sanal zamanda üretilip doğrulanmıştır.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) katalog sayfasında kullanıcı her "Apply" tıklamasında o anki filtreyi gönderiyor. Aynı filtre üst üste gelirse API'ye yeniden gitmemek için distinctUntilChanged koyduk:

import { distinctUntilChanged } from "rxjs";
import { showMarbles } from "../../tools/marble.ts";

// katalog sayfası, kullanıcı her "Apply"a bastığında o anki filtreyi yayar
showMarbles("distinctUntilChanged()", (cold) =>
  cold("-a--b--c--|", {
    a: { category: "classics" },
    b: { category: "classics" },
    c: { category: "fantasy" },
  }).pipe(distinctUntilChanged()),
);

Kullanıcı iki kez üst üste "classics" seçti, sonra "fantasy". distinctUntilChanged'den kaç değer geçer ve API'ye kaç istek gider? cold("-a--b--c--|") satırını henüz okuyamıyorsan sorun değil; bu yazının ilk işi o.

Angular serisinin "Angular'da RxJS'e Giriş" yazısında map, filter ve debounceTime'ı bir arama kutusunda kullandık. Bu yazıda temel operator'leri tek tek ve edge case'leriyle ele alıyoruz: dönüşüm için map, scan, reduce, pairwise; filtreleme için filter, take, takeWhile, skip, first, last, distinctUntilChanged ve distinctUntilKeyChanged. Her birinin davranışını marble diagram ile göstereceğiz.

Marble Diagram Okumak

Marble diagram, bir Observable'ın zaman içinde ne yaptığını tek satırda gösteren bir notasyon. RxJS'in dokümanı, test araçları ve bu serinin geri kalanı bu notasyonu kullanıyor:

  • - bir zaman birimi (frame), bu birimde hiçbir şey olmuyor.
  • a, b, c… bir değer (next); harfin karşılığı yanında ayrıca verilir.
  • | complete, # error.
  • (b|) parantez içindekiler aynı frame'de olur: burada b değeri ve hemen ardından complete.

Bu yazıdaki diagram'ları elle çizmedim. Snapshot'taki tools/marble.ts dosyasının showMarbles fonksiyonu, verdiğin kaynak marble'ı RxJS'in TestScheduler'ında sanal zamanda oynatıyor (bir karakter bir frame, yani bir sanal milisaniye), operator'ün çıktısını kaydedip marble olarak yazıyor ve sonra bu çıktıyı yine TestScheduler'ın expectObservable kontrolüyle doğruluyor. Yani aşağıdaki her "out" satırı RxJS'in gerçek davranışı; yanlış olsaydı script hata verirdi. Marble'ı bir test aracı olarak kullanmak, yani TestScheduler ile test yazmak, bu serinin marble testing yazısının konusu. İlk örnek:

import { map, take } from "rxjs";
import { showMarbles } from "../../tools/marble.ts";

// TRY cinsinden fiyat -> kuruş cinsinden fiyat
showMarbles("map((price) => price * 100)", (cold) =>
  cold("-a--b--c--|", { a: 450, b: 320, c: 390 }).pipe(map((price) => price * 100)),
);

// stok servisi iki değerden sonra hata veriyor: # hatadır
showMarbles("map((stock) => stock < 3)", (cold) =>
  cold("-a--b--#", { a: 12, b: 2 }, new Error("stock service down")).pipe(map((stock) => stock < 3)),
);

// take(2): ikinci değer ve complete aynı frame'e düşüyor
showMarbles("take(2)", (cold) => cold("-a--b--c--|", { a: "Dune", b: "Emma", c: "Middlemarch" }).pipe(take(2)));
map((price) => price * 100)
  in : -a--b--c--|      a=450 b=320 c=390
  out: -a--b--c--|      a=45000 b=32000 c=39000
map((stock) => stock < 3)
  in : -a--b--#         a=12 b=2
  out: -a--b--#         a=false b=true #=Error
take(2)
  in : -a--b--c--|      a=Dune b=Emma c=Middlemarch
  out: -a--(b|)         a=Dune b=Emma

Diagram'ı soldan sağa oku. map'te her değer aynı frame'de, dönüştürülmüş olarak çıkıyor; zamanlama değişmiyor. Kaynak hata verince map de aynı anda hata veriyor: operator'ler hatayı varsayılan olarak aşağı iletir. take(2)'de ise ilginç bir şey var: ikinci değerle birlikte complete geliyor ((b|)). take, saydığı değere ulaştığı anda kaynağı beklemeden tamamlanıyor ve kaynağa unsubscribe oluyor; c hiç üretilmiyor bile.

Operator, Observable Alıp Observable Döndüren Bir Fonksiyondur

İlk yazıdaki MiniObservable'da map'i kendimiz yazmıştık. RxJS'te de aynı: map(fn) bir Observable değil, bir fonksiyon döndürüyor. RxJS buna pipeable operator diyor, tipi OperatorFunction<In, Out>:

import { map, of } from "rxjs";

// operator bir fonksiyondur: Observable girer, Observable çıkar
const toMinorUnits = map((price: number) => price * 100);
console.log(typeof toMinorUnits);

// doğrudan çağırmak ...
toMinorUnits(of(450, 320)).subscribe((v) => console.log("direct call:", v));

// ... pipe'ın senin yerine yaptığı şeyin aynısı
of(390).pipe(toMinorUnits).subscribe((v) => console.log("pipe:", v));

// tek bir operator değeri, iki farklı kaynakta
const cart$ = of(450, 320);
const wishlist$ = of(280);
cart$.pipe(toMinorUnits).subscribe((v) => console.log("cart:", v));
wishlist$.pipe(toMinorUnits).subscribe((v) => console.log("wishlist:", v));
function
direct call: 45000
direct call: 32000
pipe: 39000
cart: 45000
cart: 32000
wishlist: 28000

source$.pipe(a, b, c), c(b(a(source$))) yazmanın okunur hâli. Her operator, kaynağını değiştirmiyor; kaynağa subscribe olan yeni bir Observable döndürüyor. Bu yüzden toMinorUnits gibi bir operator değeri birden çok kaynakta güvenle kullanılabiliyor ve kendi operator'lerini mevcut olanları birleştirerek yazabiliyorsun (bu serinin custom operator yazısında).

Dönüşüm: map, scan, reduce ve pairwise

Bookstore sepetinde kullanıcı Dune'u (450 TL) ve Emma'yı (320 TL) ekliyor, Dune'u çıkarıyor, The Hobbit'i (390 TL) ekliyor. Sepet değişikliklerini TL cinsinden bir akış olarak düşünelim ve ara toplamı tutalım:

import { pairwise, reduce, scan } from "rxjs";
import { showMarbles } from "../../tools/marble.ts";

// TRY cinsinden sepet değişiklikleri: +Dune, +Emma, -Dune, +The Hobbit
const cartChanges = { a: 450, b: 320, c: -450, d: 390 };

showMarbles("scan((total, change) => total + change, 0)", (cold) =>
  cold("-a--b--c--d--|", cartChanges).pipe(scan((total, change) => total + change, 0)),
);

showMarbles("reduce((total, change) => total + change, 0)", (cold) =>
  cold("-a--b--c--d--|", cartChanges).pipe(reduce((total, change) => total + change, 0)),
);

// Dune'un stok seviyesi; pairwise [önceki, şimdiki] verir
showMarbles("pairwise()", (cold) => cold("-a--b--c--|", { a: 12, b: 11, c: 9 }).pipe(pairwise()));
scan((total, change) => total + change, 0)
  in : -a--b--c--d--|   a=450 b=320 c=-450 d=390
  out: -a--b--c--d--|   a=450 b=770 c=320 d=710
reduce((total, change) => total + change, 0)
  in : -a--b--c--d--|   a=450 b=320 c=-450 d=390
  out: -------------(a|) a=710
pairwise()
  in : -a--b--c--|      a=12 b=11 c=9
  out: ----a--b--|      a=[ 12, 11 ] b=[ 11, 9 ]

scan ile reduce aynı accumulator fonksiyonunu alıyor ama farklı sorulara cevap veriyor. scan her değişiklikte o ana kadarki toplamı yayıyor: 450, 770, 320, 710. Sepet ekranındaki ara toplam tam olarak bu; scan, bir akışın içinde running state tutmanın standart yolu. reduce ise yalnız kaynak tamamlandığında tek bir sonuç veriyor. Sepet gibi tamamlanmayan bir akışta reduce hiçbir şey yaymaz; kullanıcı ekranda hiçbir zaman toplam görmez. reduce'u, sayfalı bir API'nin bütün sayfalarını toplamak gibi gerçekten biten akışlarda kullan.

pairwise her değeri bir öncekiyle eşleştiriyor; ilk değer tek başına bir şey üretmiyor. Stok 12'den 11'e, sonra 9'a düştüğünde [12, 11] ve [11, 9] geliyor. "Stok 3'ün altına yeni mi düştü?" sorusu tek değerle cevaplanamaz, önceki değer gerekir; uyarıyı yalnız eşik geçildiğinde bir kez göstermek için pairwise ve ardından bir filter yeterli.

Filtreleme: filter, takeWhile ve skip

filter en basit operator gibi görünür ama TypeScript tarafında öğreteceği bir şey var. Sepet event'leri bir union type olsun:

import { filter, map, skip, takeWhile } from "rxjs";
import { showMarbles } from "../../tools/marble.ts";

type AddToCart = { type: "add"; title: string; price: number };
type RemoveFromCart = { type: "remove"; title: string };
type CartEvent = AddToCart | RemoveFromCart;

const events: Record<string, CartEvent> = {
  a: { type: "add", title: "Dune", price: 450 },
  b: { type: "remove", title: "Dune" },
  c: { type: "add", title: "Bookmark", price: 0 }, // kampanyanın eklediği ücretsiz hediye
  d: { type: "add", title: "The Hobbit", price: 390 },
};

// açık bir type guard: bu filter'dan sonra TypeScript her değerin AddToCart olduğunu bilir
const isPaidAdd = (e: CartEvent): e is AddToCart => e.type === "add" && e.price > 0;

showMarbles("filter(isPaidAdd), map((e) => e.price)", (cold) =>
  cold("-a--b--c--d--|", events).pipe(
    filter(isPaidAdd),
    map((e) => e.price),
  ),
);

// sipariş geldikçe Middlemarch'ın stoğu
const stock = { a: 5, b: 3, c: 2, d: 4 };
showMarbles("takeWhile((s) => s >= 3)", (cold) =>
  cold("-a--b--c--d--|", stock).pipe(takeWhile((s) => s >= 3)),
);
showMarbles("takeWhile((s) => s >= 3, true)", (cold) =>
  cold("-a--b--c--d--|", stock).pipe(takeWhile((s) => s >= 3, true)),
);
showMarbles("skip(1)", (cold) => cold("-a--b--c--d--|", stock).pipe(skip(1)));
filter(isPaidAdd), map((e) => e.price)
  in : -a--b--c--d--|   a={ type: 'add', title: 'Dune', price: 450 } b={ type: 'remove', title: 'Dune' } c={ type: 'add', title: 'Bookmark', price: 0 } d={ type: 'add', title: 'The Hobbit', price: 390 }
  out: -a--------b--|   a=450 b=390
takeWhile((s) => s >= 3)
  in : -a--b--c--d--|   a=5 b=3 c=2 d=4
  out: -a--b--|         a=5 b=3
takeWhile((s) => s >= 3, true)
  in : -a--b--c--d--|   a=5 b=3 c=2 d=4
  out: -a--b--(c|)      a=5 b=3 c=2
skip(1)
  in : -a--b--c--d--|   a=5 b=3 c=2 d=4
  out: ----a--b--c--|   a=3 b=2 c=4

isPaidAdd'in dönüş tipi e is AddToCart; RxJS'in filter overload'u bu type guard'ı tanıyor ve sonraki map'te e'nin tipi AddToCart oluyor. Basit kontrollerde buna gerek kalmadı: TypeScript 5.5'ten beri filter((e) => e.type === "add") gibi bir arrow function'ın type predicate'ini derleyici kendisi çıkarıyor, TypeScript 6.0'da da denediğimde daraltma çalıştı. Ama koşul karmaşıklaşınca çıkarım duruyor. Aynı filter'ı açık guard olmadan yazınca:

filter((e) => e.type === "add" && e.price > 0),
map((e) => e.price),
src/03/05-filter.ts(21,18): error TS2339: Property 'price' does not exist on type 'CartEvent'.

Derleyici, koşul false döndüğünde değerin AddToCart olmadığını söyleyemiyor (ücretsiz hediye de bir AddToCart), bu yüzden predicate çıkarmıyor. Çözüm, yukarıdaki gibi isimli ve açık bir type guard.

takeWhile, koşul ilk kez false olduğu anda tamamlanıyor ve o değeri yaymıyor: stok 2'ye düşünce akış bitti, sonraki 4 hiç görülmedi. İkinci parametre inclusive: true verilirse koşulu bozan değer de yayılıyor; "stok eşiğin altına düştü" bilgisini kaybetmemek için bu sürüm daha kullanışlı. takeWhile ile filter'ı karıştırmamak gerekiyor: filter uymayan değeri atlayıp devam eder, takeWhile akışı sonlandırır. skip(n) ilk n değeri atıyor; Subject yazısında göreceğimiz BehaviorSubject gibi subscribe olur olmaz mevcut değeri veren kaynaklarda, yalnız sonraki değişikliklerle ilgileniyorsan işe yarıyor.

first ve last: EmptyError Tuzağı

first ve last akıştan tek bir değer alıp tamamlanıyor. Kullanımı kolay, ama boş bir akışta ne yaptıklarını bilmek gerekiyor:

import { EMPTY, first, last, of } from "rxjs";
import { showMarbles } from "../../tools/marble.ts";

const prices = { a: 450, b: 320, c: 390 };

showMarbles("first()", (cold) => cold("-a--b--c--|", prices).pipe(first()));
showMarbles("first((p) => p < 400)", (cold) => cold("-a--b--c--|", prices).pipe(first((p) => p < 400)));
showMarbles("last()", (cold) => cold("-a--b--c--|", prices).pipe(last()));
showMarbles("first() on an empty stream", (cold) => cold("----|").pipe(first()));

// 100 TL altında kitap yok: hata mesajı ve hatayı önleyen varsayılan değer
of(450, 320, 390).pipe(first((p) => p < 100)).subscribe({
  error: (err: Error) => console.log(`${err.name}: ${err.message}`),
});
EMPTY.pipe(first(undefined, "no books")).subscribe((v) => console.log("with default:", v));
first()
  in : -a--b--c--|      a=450 b=320 c=390
  out: -(a|)            a=450
first((p) => p < 400)
  in : -a--b--c--|      a=450 b=320 c=390
  out: ----(a|)         a=320
last()
  in : -a--b--c--|      a=450 b=320 c=390
  out: ----------(a|)   a=390
first() on an empty stream
  in : ----|            
  out: ----#             #=EmptyError
EmptyError: no elements in sequence
with default: no books

first(), ilk değeri alır almaz tamamlanıyor ve kaynağa unsubscribe oluyor; take(1)'e benziyor. last() ise kaynağın bitmesini beklemek zorunda, çünkü hangi değerin son olduğunu ancak complete geldiğinde bilebilir. Asıl fark boş akışta çıkıyor: kaynak hiç değer yaymadan tamamlanırsa (ya da predicate'e uyan değer yoksa) first ve last EmptyError ile hata veriyor. take(1) aynı durumda sessizce tamamlanır. Bookstore'da "kategorideki ilk indirimli kitap" gibi bir sorgu boş dönebiliyorsa first bir hata fırlatır ve hata yakalanmadıysa ilk yazıdaki gibi yakalanmamış hata olarak raporlanır. Ya ikinci parametreyle bir varsayılan değer ver, ya da boş sonucu normal kabul ediyorsan take(1) kullan.

distinctUntilChanged ve distinctUntilKeyChanged

Tahminin cevabı:

distinctUntilChanged()
  in : -a--b--c--|      a={ category: 'classics' } b={ category: 'classics' } c={ category: 'fantasy' }
  out: -a--b--c--|      a={ category: 'classics' } b={ category: 'classics' } c={ category: 'fantasy' }

Üç değerin üçü de geçti, yani API'ye üç istek gider. distinctUntilChanged varsayılan olarak === ile karşılaştırıyor; a ve b aynı içerikte ama farklı object'ler, dolayısıyla "farklı" sayılıyorlar. Formdan ya da route parametrelerinden her seferinde yeni bir object üretildiği için bu durum gerçek kodda çok sık yaşanıyor. Primitive değerlerde ise operator beklendiği gibi çalışıyor:

import { distinctUntilChanged, distinctUntilKeyChanged, map } from "rxjs";
import { showMarbles } from "../../tools/marble.ts";

// arama kutusu: aynı terim yeniden gelince yeni istek atılmamalı
showMarbles("distinctUntilChanged() on search terms", (cold) =>
  cold("-a--a--b--b--a--|", { a: "dune", b: "emma" }).pipe(distinctUntilChanged()),
);

type CatalogFilter = { category: string; maxPrice: number };
const filters: Record<string, CatalogFilter> = {
  a: { category: "classics", maxPrice: 400 },
  b: { category: "classics", maxPrice: 400 }, // aynı içerik, yeni object
  c: { category: "classics", maxPrice: 300 },
};

// 1) alanları kendin karşılaştır
showMarbles("distinctUntilChanged(sameFilter)", (cold) =>
  cold("-a--b--c--|", filters).pipe(
    distinctUntilChanged((prev, curr) => prev.category === curr.category && prev.maxPrice === curr.maxPrice),
  ),
);

// 2) yalnız bir alan önemliyse
showMarbles("distinctUntilKeyChanged('category')", (cold) =>
  cold("-a--b--c--|", filters).pipe(distinctUntilKeyChanged("category")),
);

// 3) filtreyi önce primitive bir değere çevir, ör. ürettiği query string
showMarbles("map(toQuery), distinctUntilChanged()", (cold) =>
  cold("-a--b--c--|", filters).pipe(
    map((f) => `category=${f.category}&maxPrice=${f.maxPrice}`),
    distinctUntilChanged(),
  ),
);
distinctUntilChanged() on search terms
  in : -a--a--b--b--a--| a=dune b=emma
  out: -a-----b-----a--| a=dune b=emma
distinctUntilChanged(sameFilter)
  in : -a--b--c--|      a={ category: 'classics', maxPrice: 400 } b={ category: 'classics', maxPrice: 400 } c={ category: 'classics', maxPrice: 300 }
  out: -a-----b--|      a={ category: 'classics', maxPrice: 400 } b={ category: 'classics', maxPrice: 300 }
distinctUntilKeyChanged('category')
  in : -a--b--c--|      a={ category: 'classics', maxPrice: 400 } b={ category: 'classics', maxPrice: 400 } c={ category: 'classics', maxPrice: 300 }
  out: -a--------|      a={ category: 'classics', maxPrice: 400 }
map(toQuery), distinctUntilChanged()
  in : -a--b--c--|      a={ category: 'classics', maxPrice: 400 } b={ category: 'classics', maxPrice: 400 } c={ category: 'classics', maxPrice: 300 }
  out: -a-----b--|      a=category=classics&maxPrice=400 b=category=classics&maxPrice=300

Arama terimlerinde beş değerden üçü geçti. Dikkat: sondaki "dune" ilk "dune" ile aynı olmasına rağmen geçti, çünkü distinctUntilChanged yalnız bir önceki değerle karşılaştırıyor, geçmişin tamamıyla değil. "Bu terimi daha önce hiç aradık mı?" sorusu için ayrı bir operator (distinct) var, ama sınırsız büyüyen bir Set tuttuğu için uzun ömürlü akışlarda dikkatli kullanılmalı.

Object'lerde üç seçenek var. Comparator vermek en açık olanı, ama filtreye yeni bir alan eklendiğinde comparator'ı güncellemeyi unutmak kolay: o zaman yeni alan değiştiğinde istek gitmez. distinctUntilKeyChanged('category') yalnız bir alana bakıyor; örnekte maxPrice 400'den 300'e değiştiği hâlde değer geçmedi, yani kullanıcı fiyat filtresini değiştirdiğinde liste güncellenmezdi. Burada yanlış seçim. Üçüncü yol bence en sağlamı: filtreyi önce API'ye gidecek query string'e çevirip primitive üzerinde karşılaştırmak. İsteği belirleyen şey zaten o string olduğu için, iki filtre aynı isteği üretiyorsa aynı sayılıyor; yeni alan eklendiğinde de karşılaştırma kendiliğinden güncel kalıyor.

Üretimde Dikkat

Bu operator'lerin çoğunun maliyeti sıfıra yakın, ama yanlış seçimin bedeli ağda ve kullanıcı deneyiminde ödeniyor. Bookstore'un katalog sayfasında distinctUntilChanged'in object'lerde çalışmaması, her "Apply" tıklamasında aynı sorgunun tekrar gitmesi demek. Bunu ölçmenin yolu basit: DevTools'un Network sekmesinde aynı query string ile art arda giden istekleri say, ya da service katmanında istek sayan bir tap ile log at. Sayı, kullanıcının gerçek filtre değişikliği sayısından fazlaysa karşılaştırma yanlış.

scan ile tutulan state'in büyüklüğüne de dikkat et. Sepet toplamı gibi tek bir sayı sorun değil; ama scan((all, item) => [...all, item], []) ile her değeri bir array'e biriktirirsen, tamamlanmayan bir akışta bu array sayfa açık kaldığı sürece büyür ve her yeni değerde baştan kopyalanır. Gerçekten tüm geçmiş gerekiyorsa üst sınır koy; çoğu zaman yalnız son n değer yeterlidir.

Son olarak first, last ve single gibi "tam olarak şu kadar değer bekliyorum" diyen operator'ler, beklenti karşılanmadığında hata fırlatır. Production'da bu hataların görünür olması için ilk yazıdaki config.onUnhandledError ayarını bir log sistemine bağlamak, "liste neden boş geldi" sorusunun cevabını hızlandırır.

Sık Düşülen Tuzaklar

  • Boş olabilecek bir akışta first() kullanmak. Kaynak değer yaymadan tamamlanırsa EmptyError gelir; varsayılan değer ver ya da take(1) kullan.
  • distinctUntilChanged'in object içeriğini karşılaştırdığını sanmak. Varsayılan karşılaştırma ===; object'lerde comparator ver ya da önce primitive bir değere (query string, id) çevir.
  • distinctUntilKeyChanged ile yalnız bir alanı izleyip diğer alanların değişikliğini kaçırmak.
  • Tamamlanmayan bir akışta reduce ya da last kullanıp değer beklemek. İkisi de complete gelene kadar hiçbir şey yaymaz; running state için scan.
  • takeWhile'ın koşulu bozan değeri de yaydığını sanmak. Varsayılan olarak yaymaz; o değer de gerekiyorsa inclusive: true.
  • Karmaşık bir filter koşulunda TypeScript'in tipi daraltacağını varsaymak. Koşul birden çok alana bakıyorsa açık bir type guard (e is AddToCart) yaz.

Küçük Bir Deneme

★☆☆ 04-transform.ts'teki sepet örneğine, toplam 1000 TL'yi ilk kez geçtiğinde "free shipping unlocked" mesajını bir kez yayan bir zincir ekle. scan, map ve first yeterli. Sonra değişiklik akışını toplam hiç 1000'i geçmeyecek şekilde değiştir ve first'ün ne yaptığını marble çıktısında gör.

★★☆ pairwise kullanarak Dune'un stoğu 3'ün altına yeni düştüğünde (önceki değer 3 veya üstü, yenisi 3'ün altı) tek bir uyarı yayan bir operator zinciri yaz. Kaynağı "-a--b--c--d--e--|" ve { a: 5, b: 2, c: 1, d: 4, e: 2 } ile dene; kaç uyarı bekliyorsun?

★★★ Katalog filtresine sort: "price-asc" | "price-desc" alanı ekle. Comparator'lı distinctUntilChanged çözümünün yeni alanı neden kaçırdığını marble çıktısıyla göster, sonra query string çözümünün değişiklik gerektirmeden doğru çalıştığını doğrula. Query string'deki parametre sırası değişirse ne olur?

Bu yazıdaki operator'lerin hepsi bir değeri başka bir değere çeviriyordu. Gerçek uygulamada çoğu zaman bir değer bir istek başlatır: arama terimi bir HTTP çağrısına, "Siparişi Ver" tıklaması bir POST'a dönüşür. O zaman Observable içinde Observable oluşur ve "önceki istek bitmeden yenisi gelirse ne olacak?" sorusu ortaya çıkar. Bir sonraki yazıda switchMap, mergeMap, concatMap ve exhaustMap arasındaki farkı bu soruyla ele alacağız.