AngularAtlased. 22.2

Ch. 03Signals & reactivity18 / 75

Lists: search, filter, sort, paginate

A chain of computed signals that recalculates only what changed, with the filters kept in the URL so a link reopens the same view.

~0 min read Official docs

Almost every app has a list you can search, filter, sort and page through. With signals it takes a few lines: the filters are signals, and the list on screen is a computed built from them. The interesting part is how you split that computed.

This page builds a catalog step by step: a chain of computed signals that recalculates only what changed, linkedSignal to reset the page, and filters kept in the URL so that a link or a reload brings back the same view.

Why it matters
A list that recalculates everything on every keystroke is fine with 50 items and stutters with 5,000. A list whose filters live only in memory loses them on reload, and its Back button does something nobody expects.

The state: a few signals

Each control writes one signal: query, category, sort, page. Nothing else is stored — the visible list is always derived, so it can never drift away from the filters.

export class Catalog {
  private readonly products = inject(ProductStore).all; // Signal<Product[]>

  // one signal per control
  readonly query = signal('');
  readonly category = signal<Category | 'all'>('all');
  readonly sort = signal<Sort>('relevance');
  readonly page = signal(1);
}

A chain of computed signals

Put each step in its own computed: filtered reads the data and the filters, sorted reads filtered and the sort order, visible reads sorted and the page. A computed runs again only when something it read has changed, so turning the page re-runs only the slice.

// each step reads only what it needs — and re-runs only when that changes
readonly filtered = computed(() => {
  const q = fold(this.query().trim());
  const cat = this.category();
  return this.products().filter(
    (p) => (cat === 'all' || p.category === cat) && fold(p.name).includes(q),
  );
});

readonly sorted = computed(() =>
  this.filtered().toSorted(comparator(this.sort())), // a copy: never sort the signal's array
);

readonly visible = computed(() => {
  const start = (this.page() - 1) * PAGE_SIZE;
  return this.sorted().slice(start, start + PAGE_SIZE); // the page changes → only this runs
});

readonly pages = computed(() => Math.max(1, Math.ceil(this.sorted().length / PAGE_SIZE)));
LiveThe same catalog in two shapes
PhonePhones & tablets€351.99★ 2.6
Phone MiniPhones & tablets€509.99★ 2.7
Phone AirPhones & tablets€552.99★ 4.3
Phone ProPhones & tablets€920.99★ 4.6
Phone MaxPhones & tablets€778.99★ 4.8
TabletPhones & tablets€377.99★ 3.9
Page 1 of 11 · 65 products
StepRunsLast run
filtered1checked 65 products
sorted164 comparisons
visible1took 6 items
Try: next page → only visible runs.
Type in the search box, change the sort order, then go through the pages — and watch the Runs column. Then switch to one big computed and do the same.

In the chain, the page buttons re-run only visible; sorting re-runs sorted and visible; only the filters reach filtered. In one big computed, every click filters and sorts all 65 products again. Order matters too: filter first (fewer items to sort), slice last. And only the shape on screen runs at all: a computed that nobody reads never executes. Type quickly and you may see fewer runs than keystrokes: a computed recalculates only when it's read, and Angular reads it once per render.

Back to page 1 when the filters change

If you're on page 5 and search for something with two pages of results, you end up on “page 5 of 2”. linkedSignal is made for this: a writable signal that resets itself whenever its source changes — here, any filter or the sort order. See linkedSignal.

// a writable signal that goes back to 1 whenever a filter or the sort order changes
readonly page = linkedSignal({
  source: () => [this.query(), this.category(), this.sort()],
  computation: () => 1,
});

next() {
  this.page.update((p) => Math.min(p + 1, this.pages()));
}

Search that feels right

Compare normalized text, lowercase and without diacritics (normalize('NFD')), so that “cafe” finds “Café”. Sort names with localeCompare in the user's language. For searches that go to a server, wait for a pause in typing with debounced().

/** lowercase, without diacritics: "cafe" finds "Café", "casti" finds "Căști" */
export function fold(text: string): string {
  return text.normalize('NFD').replace(/\p{Diacritic}/gu, '').toLowerCase();
}

// names sorted the way the user expects in their language
const byName = (a: Product, b: Product) => a.name.localeCompare(b.name, locale);

Rendering: track by id

@for (p of visible(); track p.id) lets Angular move existing rows instead of recreating them when the order changes. With track $index, every row would be re-rendered after each sort. More in control flow.

<input type="search" [value]="query()" (input)="query.set(search.value)" #search />

@for (p of visible(); track p.id) {
  <app-product-row [product]="p" />
} @empty {
  <p>No products match.</p>
}

<button (click)="page.set(page() - 1)" [disabled]="page() === 1">Previous</button>
<span>Page {{ page() }} of {{ pages() }}</span>
<button (click)="page.set(page() + 1)" [disabled]="page() === pages()">Next</button>

Filters in the URL

Filters held only in signals disappear on reload and can't be shared. Put them in the query string — /catalog?q=pro&cat=audio — and the URL becomes the state: a link reopens the same view, and Back returns to the previous view instead of leaving the page.

LiveWhere the filters live: memory or the address bar
/catalog

65 results · Phone, Phone Mini, Phone Air

Browser history
  1. /catalog
Link to this view

Whoever opens this link sees exactly what the link contains — nothing more.

In each mode, type a few letters, change the category, then use Back, Reload and Open the link.

With signals only, Back leaves the catalog and Reload empties it. Pushing every change into the history fixes that, but turns Back into an undo for every keystroke. The usual compromise: replace the current history entry while the user types, and push a new one for discrete choices such as a category.

// app.config.ts: provideRouter(routes, withComponentInputBinding())

export class Catalog {
  private readonly router = inject(Router);

  // ?q=…&cat=… arrive as inputs; the URL is the source of truth
  readonly q = input('');
  readonly cat = input<Category | 'all'>('all');

  search(text: string) {
    // typing: replace the current history entry, don't add one per key
    this.update({ q: text || null }, true);
  }

  pickCategory(cat: Category | 'all') {
    this.update({ cat: cat === 'all' ? null : cat }, false); // a real step: Back returns here
  }

  private update(queryParams: Params, replaceUrl: boolean) {
    // null removes a parameter: default values stay out of the URL
    this.router.navigate([], { queryParams, queryParamsHandling: 'merge', replaceUrl });
  }
}

With withComponentInputBinding(), query parameters arrive as component inputs; writing them back is a router.navigate() with queryParamsHandling: 'merge'. Keep the URL short by leaving out default values, as the lab does.

When the list gets huge

Past a few thousand rows, rendering becomes the bottleneck, not filtering: render only what's on screen with virtual scroll. When the data lives on the server, send the filters as request parameters and let httpResource reload when they change.

Common pitfalls

  • Sorting the signal's array in place. list.sort() mutates it. Copy first ([...list] or toSorted()), or the next filter works on a reordered source.
  • Storing the filtered list in a signal. Then every control has to remember to update it. Derive it with computed.
  • “Page 5 of 2”. The page didn't reset when the filters changed. Use linkedSignal.
  • One history entry per keystroke. Use replaceUrl: true while the user types.
  • track $index on a sortable list. Every sort re-renders every row and loses focus and input state.

Cheat sheet

state                  one signal per control: query, category, sort, page
derived                filtered → sorted → visible, each its own computed()
order                  filter first, sort fewer items, slice last
lazy                   a computed nobody reads never runs
reset the page         linkedSignal({ source: () => [filters…], computation: () => 1 })
never mutate           toSorted() / [...list].sort(); the signal's array stays as it was
search                 normalize('NFD') + lowercase; localeCompare(b, locale) for names
rendering              @for (… ; track p.id), never track $index on a sortable list
URL                    ?q=&cat=&sort=; withComponentInputBinding(); omit default values
history                replaceUrl while typing, push for real steps (category, page)
huge lists             virtual scroll for rendering; httpResource when the server filters

Ch. 03 · 0 of 8 complete

Finished this lesson?

Mark it and it shows up on your map: in the contents and on the home page.

Marks itself as done once you have read the whole lesson.

  1. signal() and computed()
  2. effect() — when YES, when NO
  3. linkedSignal() — editable derived state
  4. resource, rxResource, httpResource
  5. debounced() — delayed signals
  6. Lists: search, filter, sort, paginate
  7. Observables from zero
  8. Signals ↔ RxJS
See your whole map

All 75 pages · type to filter

  • 01What's new in Angular 21 & 22All the important changes in v21 and v22, briefly, with a link to the page that explains each one.Start
  • 01Your first app, step by stepA reading list built from an empty component to a finished app — signals, @for, inputs and outputs, a service and a form, one step at a time.Start
  • 01Quick decisions“Where do I put this?”, “Signal or Observable?”, “Shared or feature?” — short answers to everyday questions.Start
  • 01Angular interview questions60 real interview questions for junior, mid and senior, with the answer to give, the follow-up and the red flags. Plus a mock interview.Start
  • 02Standalone & bootstrapHow an app starts without NgModule: bootstrapApplication, app.config.ts, imports in the component.Modern fundamentals
  • 02Data binding & eventsInterpolation, property and attribute bindings, events with $event and key filters, and how [( )] is built — what goes into the DOM and what comes back.Modern fundamentals
  • 02Control flow: @if, @for, @switchThe block syntax that replaces *ngIf/*ngFor/ngSwitch, with track, @empty and exhaustive checks.Modern fundamentals
  • 02@let and the new template expressionsLocal variables with @let, arrow functions, spread, regex, instanceof, comments inside tags.Modern fundamentals
  • 02@defer — lazy loading in templatesLoad heavy components only when needed: triggers, prefetch, placeholder, loading, error.Modern fundamentals
  • 02Class & style bindingsWhy [class.x] and [style.x] replaced ngClass and ngStyle (with automatic migration from v21).Modern fundamentals
  • 02Component styles & encapsulation:host, the three encapsulation modes, theming with CSS variables, and why ::ng-deep is a last resort.Modern fundamentals
  • 02Pipes: when and howPure vs impure pipes, custom pipes, and how you choose between a pipe, computed() and a method.Modern fundamentals
  • 03signal() and computed()The reactive primitives: writable state, derived state, equality, untracked and immutability rules.Signals & reactivity
  • 03effect() — when YES, when NOSide effects synchronised with signals: localStorage, logging, imperative APIs. And their traps.Signals & reactivity
  • 03linkedSignal() — editable derived stateA signal that resets when its source changes, but that you can also set manually.Signals & reactivity
  • 03resource, rxResource, httpResourceAsync data as signals: status, value, error, reload, automatic cancellation. Stable since v22.Signals & reactivity
  • 03debounced() — delayed signalsThe “debounce” version of a signal, without RxJS. Perfect for search.Signals & reactivity
  • 03Lists: search, filter, sort, paginateA chain of computed signals that recalculates only what changed, with the filters kept in the URL so a link reopens the same view.Signals & reactivity
  • 03Observables from zeroWhat an Observable is, subscribe and unsubscribe, the operators you use every week, cold vs hot — and when a signal is enough.Signals & reactivity
  • 03Signals ↔ RxJStoSignal, toObservable, takeUntilDestroyed, outputFromObservable — and when RxJS remains the right choice.Signals & reactivity
  • 04input() and output()Inputs as signals (required, transform, alias) and typed outputs. Data down, events up.Components: communication
  • 04model() — two-way bindingBanana-in-a-box [( )] with a single API. When to use it, and when input + output is better.Components: communication
  • 04viewChild & contentChildQueries as signals: viewChild, viewChildren, contentChild, contentChildren, the read option.Components: communication
  • 04Lifecycle & DestroyRefWhich hooks still matter in the signals era, afterNextRender, DestroyRef and typed ngOnChanges.Components: communication
  • 04Content projectionng-content with select, multiple slots, default content and the trap of conditional projection.Components: communication
  • 04ng-template & ngTemplateOutletTemplates as values: render props in Angular, typed context and customising components.Components: communication
  • 05Smart vs DumbContainer vs presentational: who holds the state, who only displays, and how to split them properly.Component design
  • 05Generic components <T>A correctly typed DataTable<T> or Select<T>, with templates for cells.Component design
  • 05Dynamic componentsNgComponentOutlet, createComponent with inputBinding/outputBinding, and a dashboard built from config.Component design
  • 05Directives & host compositionAttribute directives, the host: {} metadata and the Directive Composition API (hostDirectives).Component design
  • 05Custom form controlsFormValueControl (Signal Forms) vs ControlValueAccessor: a star rating, in both variants.Component design
  • 05Change detectionOnPush is the default from v22, zone.js is gone from v21. What triggers rendering and how not to get burned.Component design
  • 06inject() & @Service()The new @Service() decorator from v22, inject() vs the constructor, and the injection context.Dependency Injection
  • 06Providers & the injector hierarchyRoot vs route vs component, useClass/useValue/useFactory/useExisting, and the optional/self/skipSelf modifiers.Dependency Injection
  • 06InjectionToken & provideX()Typed configuration, provideFeature() functions, makeEnvironmentProviders and initializers.Dependency Injection
  • 06injectAsync() — lazy servicesLoad a heavy service only when you need it, with prefetch on idle.Dependency Injection
  • 07Folder structurecore / shared / features, what goes into each, and what a feature looks like inside.Application architecture
  • 07Shared vs featureWhen to move a component into shared and (especially) when NOT to. A concrete checklist.Application architecture
  • 07Interfaces & modelsInline interface or separate file? DTO vs domain model, type vs interface, where to put them.Application architecture
  • 07State management with signalsSimple signal stores, facades, local vs global state, and when you need NgRx.Application architecture
  • 07Style guide & namingThe current official conventions: file names without suffixes, protected, readonly, member order.Application architecture
  • 07Barrels, aliases & boundariesindex.ts, path aliases, a feature’s public API and dependency rules that can be checked automatically.Application architecture
  • 07Errors: @boundary & ErrorHandlerThe new @boundary block from v22.2, the global ErrorHandler, HTTP errors and errors from resource.Application architecture
  • 08Modern routing & lazy loadingloadComponent, child routes, parameters as inputs and parameter inheritance (the new default in v22).Routing
  • 08Guards & resolverscanActivate, canMatch, canDeactivate and resolve as functions, with redirects via UrlTree / RedirectCommand.Routing
  • 09Signal Forms — the basicsForms built on signals: form(), [formField], schema-based validation. Stable since v22.Forms
  • 09Signal Forms — advancedCustom and cross-field validation, async/HTTP with debounce, dynamic lists, disabled/hidden, reusable schemas.Forms
  • 09Typed Reactive FormsTyped FormGroup, nonNullable, FormArray — and how they coexist with Signal Forms.Forms
  • 10HttpClient, interceptors & the API layerHttpClient without setup (v21), Fetch by default (v22), functional interceptors and an API layer with DTO mapping.HTTP & data
  • 11Performance — a checklistWhat really matters: OnPush + signals, track, @defer, lazy routes, images, bundles and hydration.Quality & performance
  • 11Testing with VitestVitest is the default runner from v21. TestBed, components with signals, services, HTTP and harnesses.Quality & performance
  • 11DevTools: look inside your appChrome DevTools and Angular DevTools step by step: what each panel shows, with exercises that work on this site.Quality & performance
  • 11Angular AriaHeadless, accessible components that you style however you like: tabs, accordion, menu, listbox...Quality & performance
  • 12SSR, prerender & render modesWhen to render on the server, when at build time and when only in the browser: RenderMode per route, app.routes.server.ts and the real costs.SSR & hydration
  • 12Hydration: event replay & incrementalHow the browser takes over server HTML without re-rendering it: full hydration, event replay, @defer (hydrate on …) and mismatch errors.SSR & hydration
  • 12Server-compatible codewindow doesn’t exist on the server: PLATFORM_ID, afterNextRender, DOCUMENT, TransferState, the HTTP cache and PendingTasks.SSR & hydration
  • 13Micro frontendsWhen it is worth splitting an app into several deployments and when it is just complexity: Native Federation, Module Federation, iframes, Web Components.Architecture at scale
  • 13Web Components with Angular ElementscreateCustomElement: an Angular component used as an HTML tag anywhere, plus communication between apps through events and BroadcastChannel.Architecture at scale
  • 13Monorepo, libraries & boundariesA workspace with several apps and libraries: ng generate library, Nx, secondary entry points, publishing and dependency rules.Architecture at scale
  • 13Hexagonal architecture (ports & adapters)A domain without Angular, ports as abstract classes and interchangeable adapters: HTTP, localStorage or in-memory, without touching the UI.Architecture at scale
  • 14Signals under the hoodThe reactive graph: producers and consumers, push-pull, why computed is lazy and glitch-free, the diamond problem and when effects run.Advanced reactivity
  • 14Advanced RxJS: higher-order operatorsswitchMap, mergeMap, concatMap and exhaustMap on a live diagram, plus retry with backoff, shareReplay and how to avoid leaks.Advanced reactivity
  • 14NgRx SignalStoreLarge-scale state management on signals: withState, withComputed, withMethods, rxMethod, entities and hooks — and when a simple store is enough.Advanced reactivity
  • 14State machines with signalsNo more impossible states: a typed machine on signals, with transitions, guards and a live state diagram.Advanced reactivity
  • 15WebSockets: real time that survives the networkA typed socket as signals: reconnect with backoff and jitter, heartbeat, an outbox while offline, and SSR safety.Real time & offline
  • 15Optimistic updates & rollbackAn instant UI that stays honest: snapshot and rollback, out-of-order responses, idempotency keys.Real time & offline
  • 15Offline-first: Service Worker & cachingThe Angular Service Worker: caching strategies, update prompts and an outbox for changes made offline.Real time & offline
  • 16Web WorkersHeavy computation without blocking the UI: a real worker in the page, message passing and when it is not worth it.Advanced UI & performance
  • 16Huge lists: virtual scroll100,000 rows without lag: CDK virtual scroll, itemSize, trackBy and the alternatives (pagination, infinite scroll).Advanced UI & performance
  • 16Modern animations & View Transitionsanimate.enter and animate.leave instead of the deprecated @angular/animations, CSS transitions and View Transitions on navigation.Advanced UI & performance
  • 16Memory leaks: finding and fixing themSubscriptions, timers and listeners that outlive their component — how to spot them in DevTools and close them.Advanced UI & performance
  • 17Security: XSS, sanitisation & CSPWhat Angular does automatically against XSS, when bypassSecurityTrust becomes dangerous, plus CSP with nonce, Trusted Types and XSRF.Security & globalisation
  • 17Authentication: tokens & refreshAn interceptor that attaches the token, refreshes once on 401 and replays the queued requests — plus where you keep the token.Security & globalisation
  • 17OAuth 2.1 & OpenID Connect with PKCEThe authorization code flow with PKCE, step by step: verifier and challenge, state and nonce, the ID token, and why a BFF is safer.Security & globalisation
  • 17Internationalisation (i18n)$localize, the i18n attribute, ICU plural/select, a build per language vs runtime translations, and locale-based formatting.Security & globalisation