Reaktive Filter und Sortierung
Filter und Sortierung wirken im Frontend oft harmlos.
Ein Dropdown hier. Ein paar Checkboxen dort. Ein Sortierbutton daneben.
Und schnell entsteht Code wie dieser:
visibleArticles = articles .filter(({ category }) => selectedCategories.includes(category)) .sort((left, right) => left.title.localeCompare(right.title));Funktioniert.
Zumindest am Anfang.
Später kommen weitere Filter dazu. Status. Autor. Kategorie. Tags. Veröffentlichungszustand. Sortierung nach Titel, Autor oder Kategorie. Vielleicht noch gespeicherte Filter. Vielleicht ein „Alle zurücksetzen“-Button.
Und plötzlich liegt Listenlogik überall.
In der Komponente. Im Template. In Event-Handlern. In kleinen Hilfsservices. In Arrays, die nebenbei mutiert werden.
Das Problem ist nicht filter().
Das Problem ist, wenn die Komponente entscheidet, was sichtbar ist.
Das typische Anti-Pattern
Abschnitt betitelt „Das typische Anti-Pattern“Ein klassischer Einstieg sieht oft so aus:
selectedCategories: ArticleCategory[] = [];visibleArticles: Article[] = [];
toggleCategory(category: ArticleCategory): void { if (this.selectedCategories.includes(category)) { this.selectedCategories = this.selectedCategories.filter( (selectedCategory) => selectedCategory !== category, ); } else { this.selectedCategories = [...this.selectedCategories, category]; }
this.visibleArticles = this.articles.filter(({ category }) => this.selectedCategories.includes(category), );}Oder Sortierung wird direkt im Click-Handler erledigt:
sortByTitle(): void { this.visibleArticles.sort((left, right) => left.title.localeCompare(right.title), );}Das wirkt pragmatisch.
Aber es hat mehrere unangenehme Folgen:
Die Filterlogik verteilt sich. Sortierung mutiert Arrays. UI-State ist nicht explizit modelliert. Komponenten werden zu kleinen Controllern. Tests werden unnötig schwer. Die sichtbare Liste wird ein weiterer Zustand, der synchron gehalten werden muss.
Die Komponente weiß dann nicht nur:
Der Benutzer hat einen Kategorie-Filter verändert.
Sie weiß auch:
Wie diese Filterbedingung auf Artikel angewendet wird. Wie das Ergebnis sortiert wird. Welche Liste danach sichtbar ist.
Damit übernimmt die Komponente Verantwortung, die nicht zur Komponente gehört.
Die Kernaussage
Abschnitt betitelt „Die Kernaussage“Filter sind Zustand.
Sortierung ist Zustand.
Die sichtbare Liste ist eine Ableitung.
Das ist der zentrale Perspektivwechsel.
Der Benutzer verändert nicht die Liste. Der Benutzer verändert Filterbedingungen.
Der Benutzer sortiert nicht das Array. Der Benutzer verändert Sortierzustand.
Die sichtbare Liste ist die Folge dieses Query-State.
Wo dieser Artikel beginnt
Abschnitt betitelt „Wo dieser Artikel beginnt“Dieser Artikel beginnt nicht beim HTTP-Call.
Er beginnt dort, wo die Infrastructure die bereits geladenen Artikel als sicheres, benanntes Signal bereitstellt.
private httpResource → ArticleResource → articles: Signal<readonly Article[] | null>Der Retrieve-Slice wurde vorher behandelt. Infrastructure, HTTP-Details, API-Mapping und der technische Lifecycle der httpResource sind hier nicht Thema.
Wir nehmen bewusst an:
ArticleResource kapselt die konkrete Angular Resource.
Der Store kann auf das benannte articles-Signal zugreifen.
Jetzt geht es nur darum, wie daraus eine sichtbare, gefilterte und sortierte Liste entsteht.
Zielbild
Abschnitt betitelt „Zielbild“Wir bauen keine Suche.
Eine Suche wäre eher:
User tippt Suchtext → searchChanged({ searchText })In diesem Artikel geht es um klassische Filter:
Der Benutzer wählt eine oder mehrere Filterbedingungen aus.
Zum Beispiel Kategorien:
User öffnet Kategorie-Dropdown → wählt „Architecture“ → categoryFilterToggled({ category: 'architecture' })
User wählt zusätzlich „Angular“ → categoryFilterToggled({ category: 'angular' })
User entfernt „Architecture“ → categoryFilterToggled({ category: 'architecture' })Der Store hält daraus expliziten Query-State:
selectedCategoriessortBysortDirectionDas ViewModel wird daraus reaktiv abgeleitet:
articles + selectedCategories + sortBy + sortDirection → filterArticles → sortArticles → ArticleListVmDie UI schickt Intents. Das ViewModel leitet ab.
Nicht die Komponente filtert. Nicht das Template sortiert. Nicht ein Service mutiert das Array.
Die Komponente meldet nur:
Diese Filterbedingung wurde aktiviert oder deaktiviert.
Was daraus sichtbar wird, ist eine Ableitung im Store.

Marble 1: Intents über Zeit
Abschnitt betitelt „Marble 1: Intents über Zeit“Filter sind besonders gut über Zeit erklärbar.
Der Benutzer klickt nicht „die Liste neu“. Er verändert nacheinander Filterbedingungen.
Zeit ─────────────────────────────────────────────▶
UI Intent ──●────────●────────────●──────────────●────────▶ │ │ │ │ │ │ │ └─ sortingChanged(title, asc) │ │ └──────────────── categoryFilterToggled(architecture) │ └──────────────────────────── categoryFilterToggled(angular) └──────────────────────────────────── categoryFilterToggled(architecture)Die einzelnen Events sind nicht die fertige Liste.
Sie sind Absichten.

Ein einzelner Klick sagt nur:
Diese Kategorie wurde umgeschaltet.
Er sagt nicht:
Hier ist die neue sichtbare Liste.
Das ist wichtig.
Denn die sichtbare Liste entsteht nicht im Event. Sie entsteht aus dem Zustand nach dem Event.
Events sind fachliche Intents
Abschnitt betitelt „Events sind fachliche Intents“Ein häufiger Fehler ist, UI-Mechanik als Event zu modellieren.
dropdownChanged;checkboxClicked;buttonPressed;Das beschreibt, was technisch passiert ist.
Für den Store ist aber interessanter, was fachlich gemeint war.
categoryFilterToggled;categoryFiltersCleared;sortingChanged;Das Dropdown ist nur die Darstellung.
Der fachliche Intent lautet:
Eine Kategorie wurde als Filterbedingung aktiviert oder deaktiviert. Alle Kategorie-Filter wurden entfernt. Die Sortierung wurde geändert.
Beispiel mit NgRx Signal Events:
import { eventGroup, type } from '@ngrx/signals/events';
export const articleFilterEvents = eventGroup({ source: 'Article List Filter', events: { categoryFilterToggled: type<{ readonly category: ArticleCategory; }>(),
categoryFiltersCleared: type<void>(), },});
export const articleSortingEvents = eventGroup({ source: 'Article List Sorting', events: { sortingChanged: type<{ readonly sortBy: ArticleSortBy; readonly sortDirection: SortDirection; }>(), },});Die Namen sind bewusst fachlich.
Nicht:
inputChanged;dropdownChanged;clickSortButton;Sondern:
categoryFilterToggled;categoryFiltersCleared;sortingChanged;Das macht die UI austauschbarer.
Ob der Filter aus einem Dropdown, einer Checkboxliste, gespeicherten Views oder später aus Query-Parametern kommt, ist für den Store nicht entscheidend.
Query-State im Store
Abschnitt betitelt „Query-State im Store“Filterbedingungen sind Zustand.
Bei einem klassischen Mehrfachfilter ist das nicht ein Suchtext, sondern eine Menge aktiver Bedingungen.
export type ArticleCategory = 'architecture' | 'angular' | 'testing' | 'design';
export type ArticleSortBy = 'title' | 'author' | 'category';
export type SortDirection = 'asc' | 'desc';
export interface ArticleListQueryState { readonly selectedCategories: readonly ArticleCategory[]; readonly sortBy: ArticleSortBy; readonly sortDirection: SortDirection;}
export const initialArticleListQueryState: ArticleListQueryState = { selectedCategories: [], sortBy: 'title', sortDirection: 'asc',};selectedCategories beschreibt nicht die Artikel selbst.
Es beschreibt, durch welche Brille der Benutzer die bereits geladenen Artikel gerade betrachtet.
Keine ausgewählte Kategorie bedeutet:
Es gibt keine Kategorie-Einschränkung.
Eine ausgewählte Kategorie bedeutet:
Zeige Artikel, die zu dieser Kategorie passen.
Mehrere ausgewählte Kategorien bedeuten:
Zeige Artikel, die zu einer dieser Kategorien passen.
Ob diese Semantik OR oder AND ist, ist eine bewusste Produktentscheidung. Bei Kategorien ist meistens OR naheliegend. Bei komplexeren Facetten kann jede Filtergruppe eigene Regeln haben.
Marble 2: Query-State verändert sich
Abschnitt betitelt „Marble 2: Query-State verändert sich“Aus Events entsteht Query-State.
Nicht direkt eine Liste.
Zeit ─────────────────────────────────────────────▶
UI Intent ──●────────────●────────────●──────────────────▶ toggle A toggle B toggle A
selectedCategories ──[]───────[A]────────[A,B]────────[B]─────────▶Das ist der eigentliche Zustandswechsel.

Das ist für Backend- und OOP-geprägte Entwickler oft der wichtigste Schritt.
Wir behandeln die Liste nicht als Objekt, das aktiv umgebaut wird.
Wir behandeln die Filterbedingungen als Zustand. Die Liste ist eine Projektion daraus.
Das ViewModel
Abschnitt betitelt „Das ViewModel“Die Komponente braucht kein rohes Article[].
Sie braucht ein ViewModel.
export interface ArticleListVm { readonly selectedCategories: readonly ArticleCategory[]; readonly sortBy: ArticleSortBy; readonly sortDirection: SortDirection; readonly items: readonly ArticleListItemVm[];}
export interface ArticleListItemVm { readonly id: string; readonly title: string; readonly authorName: string; readonly category: ArticleCategory; readonly categoryLabel: string;}Das ViewModel enthält zwei Dinge:
Den aktuellen Query-State für die UI. Die daraus abgeleiteten sichtbaren Items.
Damit kann das Template kontrolliert rendern:
Das Dropdown weiß, welche Kategorien aktiv sind.
Die Sortierbuttons wissen, welcher Zustand aktiv ist.
Die Liste rendert vm.items.
Mehr muss das Template nicht wissen.
Pure Functions statt Component-Logik
Abschnitt betitelt „Pure Functions statt Component-Logik“Die eigentliche Ableitung lässt sich sauber als pure Funktionen ausdrücken.
export const filterArticles = ({ articles, selectedCategories,}: { readonly articles: readonly Article[]; readonly selectedCategories: readonly ArticleCategory[];}): readonly Article[] => { if (selectedCategories.length === 0) { return articles; }
return articles.filter(({ category }) => selectedCategories.includes(category), );};Die Filterfunktion verändert nichts.
Sie erzeugt eine Projektion auf vorhandene Daten.
Sortierung muss genauso vorsichtig sein.
Nicht so:
articles.sort((left, right) => left.title.localeCompare(right.title));sort() mutiert das Array.
Das ist in reaktiven Ableitungen gefährlich, weil dadurch die ursprünglichen Resource-Daten verändert werden können. Aus einer Projektion wird plötzlich ein Seiteneffekt.
Besser:
export const sortArticles = ({ articles, sortBy, sortDirection,}: { readonly articles: readonly Article[]; readonly sortBy: ArticleSortBy; readonly sortDirection: SortDirection;}): readonly Article[] => { const directionFactor = sortDirection === 'asc' ? 1 : -1;
return [...articles].sort( (left, right) => compareArticles({ left, right, sortBy }) * directionFactor, );};
const compareArticles = ({ left, right, sortBy,}: { readonly left: Article; readonly right: Article; readonly sortBy: ArticleSortBy;}): number => { switch (sortBy) { case 'title': return left.title.localeCompare(right.title);
case 'author': return left.author.name.localeCompare(right.author.name);
case 'category': return left.categoryLabel.localeCompare(right.categoryLabel); }};Alternativ kann in modernen Runtimes auch toSorted() verwendet werden. Der wichtige Punkt ist aber nicht die konkrete API.
Der wichtige Punkt ist:
Sortierung verändert nicht die Resource-Daten. Sortierung erzeugt eine neue Projektion.
Das ViewModel entsteht dann aus einer kleinen Pipeline:
export const toArticleListViewModel = ({ articles, selectedCategories, sortBy, sortDirection,}: { readonly articles: readonly Article[] | null; readonly selectedCategories: readonly ArticleCategory[]; readonly sortBy: ArticleSortBy; readonly sortDirection: SortDirection;}): ArticleListVm | null => { if (articles === null) { return null; }
const filteredArticles = filterArticles({ articles, selectedCategories, });
const sortedArticles = sortArticles({ articles: filteredArticles, sortBy, sortDirection, });
return { selectedCategories, sortBy, sortDirection, items: sortedArticles.map(toArticleListItemViewModel), };};
const toArticleListItemViewModel = ({ id, title, author, category, categoryLabel,}: Article): ArticleListItemVm => ({ id, title, authorName: author.name, category, categoryLabel,});Das ist testbar.
Der Mapper interpretiert keinen technischen Resource-Status.
Er projiziert nur:
readonly Article[] | null → ArticleListVm | nullDie Abwesenheit eines lesbaren Werts wurde bereits an der Infrastructure-Grenze normalisiert.
Ohne Angular. Ohne Template. Ohne Component Fixture. Ohne DOM.
Ein Test kann direkt prüfen:
Bei diesen Artikeln, diesen aktiven Kategorien und dieser Sortierung entsteht dieses ViewModel.
Marble 3: ViewModel als Ableitung
Abschnitt betitelt „Marble 3: ViewModel als Ableitung“Jetzt kommt die eigentliche reaktive Idee.
Das ViewModel entsteht aus mehreren Inputs:
Zeit ─────────────────────────────────────────────▶
articles ──●────────────────────────────────────────────▶ Article[]
selectedCategories ──[]──────[architecture]────[architecture,angular]──▶
sortState ──title asc────────────────────category desc────▶
vm() ──VM₁────VM₂──────────────────VM₃────────VM₄────▶Wenn sich einer der Inputs ändert, wird die Ableitung ungültig.
Beim nächsten Lesen entsteht ein neues ViewModel.

Das computed ist hier kein nettes Detail.
Es ist der architektonische Schnitt:
Source Signal+ Query-State= ViewModelStore mit withComputed
Abschnitt betitelt „Store mit withComputed“Jetzt wird die Ableitung im Store verdrahtet.
ArticleResource stellt die geladenen Artikel als benanntes Source Signal bereit.
Der Store hält den Query-State.
withComputed erzeugt das ViewModel.
Die konkrete HttpResourceRef bleibt vollständig in der Infrastructure.
Damit kennt der Store weder:
hasValue()- das Wurfverhalten von
value() ResourceStatus- Request- oder Parse-Details
Er konsumiert nur:
articles: Signal<readonly Article[] | null>import { computed, inject } from '@angular/core';import { patchState, signalStore, withComputed, withProps, withState,} from '@ngrx/signals';import { Events, withEventHandlers } from '@ngrx/signals/events';import { tap } from 'rxjs';
export const ArticleListStore = signalStore( withState(initialArticleListQueryState),
withProps(() => ({ _articleResource: inject(ArticleResource), })),
withComputed( ({ _articleResource, selectedCategories, sortBy, sortDirection, }) => ({ vm: computed(() => toArticleListViewModel({ articles: _articleResource.articles(), selectedCategories: selectedCategories(), sortBy: sortBy(), sortDirection: sortDirection(), }), ), }), ),
withEventHandlers((store, events = inject(Events)) => ({ toggleCategoryOnCategoryFilterToggled$: events .on(articleFilterEvents.categoryFilterToggled) .pipe( tap(({ category }) => { const selectedCategories = toggleSelectedCategory({ selectedCategories: store.selectedCategories(), category, });
patchState(store, { selectedCategories }); }), ),
clearCategoriesOnCategoryFiltersCleared$: events .on(articleFilterEvents.categoryFiltersCleared) .pipe( tap(() => { patchState(store, { selectedCategories: [] }); }), ),
setSortingOnSortingChanged$: events .on(articleSortingEvents.sortingChanged) .pipe( tap(({ sortBy, sortDirection }) => { patchState(store, { sortBy, sortDirection, }); }), ), })),);Die kleine Toggle-Funktion bleibt ebenfalls rein:
const toggleSelectedCategory = ({ selectedCategories, category,}: { readonly selectedCategories: readonly ArticleCategory[]; readonly category: ArticleCategory;}): readonly ArticleCategory[] => { if (selectedCategories.includes(category)) { return selectedCategories.filter( (selectedCategory) => selectedCategory !== category, ); }
return [...selectedCategories, category];};Die Struktur ist bewusst einfach.
withProps injiziert den Infrastructure-Adapter.
withState hält den Query-State.
withComputed leitet das ViewModel ab.
withEventHandlers reagiert auf fachliche Intents.
Der Store liest sich dadurch wie eine Architekturübersicht:
articles+ selectedCategories+ sortBy+ sortDirection → ArticleListVm
filter intent → Query-StateDie technische Absicherung der Angular Resource ist darin nicht mehr sichtbar.
Nicht, weil sie verschwunden wäre.
Sondern weil sie bereits an der technischen Quelle gekapselt wurde.
Warum hier kein leeres Array als Default?
Abschnitt betitelt „Warum hier kein leeres Array als Default?“Bei Collections ist ein leeres Array grundsätzlich ein gültiger Wert.
Für diesen Artikel bleibt die Source trotzdem zunächst:
readonly Article[] | nullDenn das Template unterscheidet aktuell zwischen:
noch kein lesbarer Wert → Placeholder
erfolgreich geladene leere Liste → ViewModel mit items: []Ein sofortiges [] als Default wäre ebenfalls möglich.
Dann müsste Loading beziehungsweise „noch nicht geladen“ über ein separates Signal vom echten Empty State unterschieden werden.
Das wäre ein anderer, ebenfalls sauberer Schnitt.
Hier bleibt null bewusst die normalisierte Abwesenheit eines lesbaren Source-Werts.
Interessant ist außerdem die Rolle von tap.
In vielen RxJS-Diskussionen wird tap zu Recht kritisch betrachtet, wenn darin irgendwo versteckt Anwendungslogik passiert. Hier ist die Situation anders.
Wir befinden uns an einer imperativen Grenze innerhalb des Stores.
Ein Event kommt rein. Der Store-State wird verändert.
Genau dafür steht hier tap:
tap(({ category }) => { const selectedCategories = toggleSelectedCategory({ selectedCategories: store.selectedCategories(), category, });
patchState(store, { selectedCategories });});Das ist kein versteckter Seiteneffekt in der Komponente.
Das ist die definierte Stelle, an der ein Intent den Query-State aktualisiert.
Außerhalb dieser Grenze bleibt die Liste eine Ableitung.
Was computed hier leistet
Abschnitt betitelt „Was computed hier leistet“Das computed liest vier Dinge:
articleResource.articles()selectedCategories()sortBy()sortDirection()Wenn sich eines davon ändert, wird die Ableitung invalidiert.
Beim nächsten Lesen entsteht ein neues ViewModel.
Das ist der zentrale Gedanke.
Der Benutzer klickt nicht die Liste um.
Er verändert selectedCategories.
Der Benutzer sortiert nicht das Array.
Er verändert sortBy und sortDirection.
Die sichtbare Liste ist die Folge dieses Zustands.
articles + selectedCategories + sortBy + sortDirection → ArticleListVmDamit arbeitet die UI nicht gegen das Framework. Sie nutzt das reaktive Modell des Frameworks.
Zustand ändert sich. Ableitungen werden ungültig. Beim Lesen entsteht die neue Projektion.
Facade: Intents nach außen anbieten
Abschnitt betitelt „Facade: Intents nach außen anbieten“Die Komponente sollte den Store nicht direkt kennen müssen.
Eine kleine Facade reicht:
import { inject, Injectable } from '@angular/core';import { injectDispatch } from '@ngrx/signals/events';
@Injectable()export class ArticleListFacade { private readonly store = inject(ArticleListStore); private readonly dispatchFilter = injectDispatch(articleFilterEvents); private readonly dispatchSorting = injectDispatch(articleSortingEvents);
readonly vm = this.store.vm;
readonly toggleCategoryFilter = (category: ArticleCategory): void => { this.dispatchFilter.categoryFilterToggled({ category }); };
readonly clearCategoryFilters = (): void => { this.dispatchFilter.categoryFiltersCleared(); };
readonly changeSorting = ({ sortBy, sortDirection, }: { readonly sortBy: ArticleSortBy; readonly sortDirection: SortDirection; }): void => { this.dispatchSorting.sortingChanged({ sortBy, sortDirection }); };}Die Facade übersetzt UI-Aufrufe in fachliche Intents.
Die Komponente muss nicht wissen, ob dahinter ein Store, Events, Signals oder später zusätzliche Logik steckt.
Sie bekommt:
vm;toggleCategoryFilter();clearCategoryFilters();changeSorting();Das ist ihre Schnittstelle.
Template: rendern, nicht rechnen
Abschnitt betitelt „Template: rendern, nicht rechnen“Das Template bleibt langweilig.
Das
@let vm = facade.vm();
@if (vm) { <button type="button" [attr.aria-pressed]="vm.selectedCategories.includes('architecture')" (click)="facade.toggleCategoryFilter('architecture')" > Architecture </button>
<button type="button" [attr.aria-pressed]="vm.selectedCategories.includes('angular')" (click)="facade.toggleCategoryFilter('angular')" > Angular </button>
<button type="button" (click)="facade.clearCategoryFilters()" > Filter zurücksetzen </button>
<button type="button" (click)=" facade.changeSorting({ sortBy: 'title', sortDirection: 'asc' }) " > Titel aufsteigend </button>
@for (item of vm.items; track item.id) { <article> <h2>{{ item.title }}</h2> <p>{{ item.authorName }}</p> <p>{{ item.categoryLabel }}</p> </article> }} @else { <app-list-placeholder />}Das Template filtert nicht.
Das Template sortiert nicht.
Das Template rendert vm.items.
Es stellt Eingaben dar und sendet Intents zurück.
Mehr nicht.
Das ist keine Einschränkung der UI. Das ist Entlastung.
Je weniger Listenlogik im Template steckt, desto klarer bleibt die Verantwortung:
Die UI zeigt den Zustand. Die UI meldet Benutzerabsichten. Der Store hält Query-State. Das ViewModel projiziert.
Warum das für OOP-geprägte Entwickler ungewohnt sein kann
Abschnitt betitelt „Warum das für OOP-geprägte Entwickler ungewohnt sein kann“In klassischer objektorientierter Denkweise liegt es nahe, eine Liste zu nehmen und Methoden darauf auszuführen.
list.activateFilter(category);list.sortByTitle();list.getVisibleItems();Oder in Komponentenlogik übersetzt:
this.visibleItems = this.items.filter(...);Das fühlt sich direkt an.
Im Frontend arbeiten moderne Frameworks aber nicht primär so. Angular, React und Vue wollen nicht, dass wir überall manuell kleine Controller bauen, die den sichtbaren Zustand synchronisieren.
Sie funktionieren besser, wenn wir ausdrücken:
Das ist der Zustand. Das ist die Ableitung daraus. Das ist das Rendering dieser Ableitung.
Bei reaktiven Filtern heißt das:
Nicht:
Wenn der Benutzer klickt, baue ich die Liste um.
Sondern:
Wenn der Benutzer klickt, ändert sich der Filterzustand. Die Liste ergibt sich daraus.
Das ist nicht akademisch.
Es reduziert Stellen, an denen Zustand auseinanderlaufen kann.
Clientseitig oder serverseitig?
Abschnitt betitelt „Clientseitig oder serverseitig?“Dieser Artikel beschreibt clientseitiges Filtern und Sortieren auf bereits geladenen Daten.
Das ist sinnvoll, wenn:
Die Datenmenge klein genug ist. Die Liste bereits geladen ist. Die Operation präsentationsnah ist. Das Ergebnis keine neue fachliche Datenabfrage erfordert.
Serverseitiges Filtern und Sortieren ist sinnvoll, wenn:
Der Payload groß ist. Pagination beteiligt ist. Berechtigungen relevant sind. Performance oder Datenmenge gegen clientseitige Verarbeitung sprechen. Die Query fachlich bestimmt, welche Daten überhaupt geladen werden dürfen.
Der Merksatz:
Client-Filter sortieren vorhandenen Zustand. Server-Filter verändern die Query der Infrastructure-Quelle.
Auch bei serverseitigem Filtern bleibt der Gedanke ähnlich: Die UI sendet Intents, der Store hält Query-State, und ArticleResource leitet daraus die technische Resource-Query ab. Nur liegt die Ableitung dann nicht mehr vollständig im Client, sondern teilweise in der Datenquelle.
Das ist ein eigenes Thema.
Hier bleiben wir bewusst bei vorhandenen Resource-Daten.
Was dadurch besser wird
Abschnitt betitelt „Was dadurch besser wird“Diese Struktur wirkt zunächst etwas ausführlicher als ein schnelles items.filter(...) in der Komponente.
Aber sie kauft Klarheit.
Der Query-State ist sichtbar.
Die Ableitung ist zentral.
Die Pure Functions sind testbar.
Die Komponente bleibt dünn.
Das Template bleibt frei von Listenlogik.
Die Source-Daten werden nicht mutiert.
Die technische httpResource bleibt in der Infrastructure.
Vor allem aber wird der mentale Schnitt klar:
UI Intent → Query-State → ViewModel-Ableitung → RenderingDas ist taktisches Frontend-Design.
Nicht, weil es kompliziert ist. Sondern weil es Verantwortungen sauber trennt.
Filter und Sortierung sind kein Component-Code.
Sie sind Query-State.
Die Liste ist kein mutiertes Array. Sie ist eine Ableitung.
Die Infrastructure stellt sichere Source Signals bereit. Die UI sendet Intent. Der Store hält Query-State und orchestriert die Projektion. Das ViewModel projiziert. Das Template rendert.
Das ist der entscheidende Perspektivwechsel:
Nicht die Komponente baut die sichtbare Liste.
Die sichtbare Liste entsteht reaktiv aus vorhandenen Artikeln, aktiven Filterbedingungen und Sortierzustand.