-
Notifications
You must be signed in to change notification settings - Fork 0
Resolvers #96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Resolvers #96
Changes from 2 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
b659930
feat(material): Add resolvers article with routing data preloading guide
d-koppenhagen 03ffbd2
feat(material resolvers): Add UX considerations and caching best prac…
d-koppenhagen 550a6c9
Apply suggestions from code review
d-koppenhagen 936c7b7
docs(material resolvers): Update code examples to use modern Signal API
d-koppenhagen 039c21a
docs(material resolvers): Clarify resolver return types and handling
d-koppenhagen 5672d3e
docs(material resolvers): Clarify type safety limitations of resolver…
d-koppenhagen 854fbd3
docs(material resolvers): Reorganize content and update code examples
d-koppenhagen 06a693a
Update material/resolvers/README.md
fmalcher 206057c
docs(material resolvers): Refine code style and clarify data access p…
fmalcher 5db4a10
date
fmalcher File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,376 @@ | ||
| --- | ||
| title: 'Resolvers: Daten beim Routing vorladen' | ||
| published: 2026-05-15 | ||
| lastModified: 2026-05-15 | ||
| --- | ||
|
|
||
| Der Angular-Router bietet mit *Resolvers* die Möglichkeit, Daten asynchron vorzuladen, bevor eine Komponente aktiviert wird. | ||
| Hier betrachten wir die Funktionsweise, Implementierung und den Zugriff auf die aufgelösten Daten in der Komponente. | ||
| Außerdem diskutieren wir, wann Resolvers sinnvoll sind – und wann nicht. | ||
|
|
||
| ## Inhalt | ||
|
|
||
| [[toc]] | ||
|
|
||
| ## Motivation | ||
|
|
||
| Asynchrone Operationen wie HTTP-Requests lösen wir in Angular üblicherweise direkt in der Komponente auf. | ||
| Mithilfe der `resource()`-API laden wir Daten reaktiv, oder wir nutzen die `AsyncPipe`, um Observables direkt im Template zu verarbeiten. | ||
| Die Komponente ist dabei sofort sichtbar, und wir können einen Ladeindikator anzeigen, während die Daten eintreffen. | ||
|
|
||
| Als Alternative bietet der Router sogenannte *Resolvers* an, um Daten schon vor dem Start der Komponente asynchron vorzuladen. | ||
| Wir geben also die Verantwortung für das Laden der Daten an den Router ab, und die Daten sind in der Komponente sofort und synchron verfügbar. | ||
|
|
||
| > **Achtung:** Trotz dieser scheinbaren Vorteile solltest du dieses Feature mit Sorgfalt verwenden! | ||
| > Ein Resolver wartet auf die Daten, bevor die Komponente geladen wird. Das kann die User Experience beeinträchtigen, weil die Navigation blockiert wird. | ||
| > Der herkömmliche Weg, Daten direkt in der Komponente zu laden, ist fast immer die bessere Wahl. | ||
|
d-koppenhagen marked this conversation as resolved.
Outdated
|
||
|
|
||
| ## Das UX-Problem mit Resolvers | ||
|
|
||
| Resolvers sind einfach zu verwenden – aber sie haben ein grundlegendes Problem: | ||
| Der Router wartet auf die asynchrone Operation, bevor die Route aktiviert wird. | ||
|
|
||
| Stell dir einen langsamen HTTP-Request vor. | ||
| Nach dem Klick auf einen Link startet der Request, aber die Navigation wird erst abgeschlossen, wenn die Antwort eintrifft. | ||
| Dauert der Request 5 Sekunden, dauert auch die Navigation 5 Sekunden. | ||
| In dieser Zeit sieht der User keine Reaktion – und klickt womöglich mehrfach auf den Link. | ||
|
|
||
| Dieses Verhalten widerspricht der Grundidee einer Single-Page-Anwendung: | ||
| Eine SPA sollte immer schnell reagieren und die Daten zur Laufzeit nachladen. | ||
| Mit Resolvers kehren wir zum Verhalten einer klassischen serverseitig gerenderten Seite zurück: Klick, warten, weiter. | ||
|
|
||
| ### Die bessere Alternative: Daten direkt in der Komponente laden | ||
|
d-koppenhagen marked this conversation as resolved.
Outdated
|
||
|
|
||
| Ohne Resolvers wird die Komponente sofort angezeigt, und die Daten werden im Hintergrund geladen. | ||
| Während der Ladezeit können wir einen Ladeindikator oder Platzhalter-Elemente (Ghost Elements) anzeigen. | ||
| Die Navigation ist sofort abgeschlossen, und der User erhält unmittelbares Feedback. | ||
|
|
||
| Mit der `resource()`-API oder `httpResource()` geht das besonders elegant: | ||
|
|
||
| ```typescript | ||
| @Component({ /* ... */ }) | ||
| export class MyComponent { | ||
| private service = inject(BookStoreService); | ||
| booksResource = this.service.getAllAsResource(); | ||
| } | ||
| ``` | ||
|
|
||
| Alternativ können wir Observables mit der `AsyncPipe` direkt im Template auflösen: | ||
|
d-koppenhagen marked this conversation as resolved.
Outdated
|
||
|
|
||
| ```typescript | ||
| @Component({ | ||
| template: ` | ||
| @if (books$ | async; as books) { | ||
| <app-book-list [books]="books" /> | ||
| } @else { | ||
| <p>Laden...</p> | ||
| } | ||
| `, | ||
| }) | ||
| export class MyComponent { | ||
| books$ = inject(BookStoreService).getAll(); | ||
| } | ||
| ``` | ||
|
|
||
| In beiden Fällen ist die Komponente sofort sichtbar, und die Daten werden asynchron nachgeladen. | ||
| Das ist fast immer die bessere Wahl gegenüber einem Resolver. | ||
|
|
||
| ## Einen Resolver definieren | ||
|
|
||
| Ein Resolver wird als Funktion mit dem Typ `ResolveFn<T>` definiert. | ||
| Der generische Typparameter `T` gibt an, welchen Datentyp das Ergebnis besitzt. | ||
| Die Funktion muss ein Observable, eine Promise oder einen direkten Wert zurückliefern. | ||
|
|
||
| Als Argumente erhält die Funktion die aktuelle Route in Form eines `ActivatedRouteSnapshot` und den Zustand des Routers als `RouterStateSnapshot`. | ||
| Wir können also z. B. Routenparameter auslesen und diese bei der Datenabfrage verarbeiten. | ||
|
|
||
| Alle Abhängigkeiten fordern wir mithilfe von `inject()` an. | ||
| Das Observable mit dem Ergebnis geben wir direkt aus der Funktion zurück – um die Subscription kümmert sich der Router automatisch. | ||
|
d-koppenhagen marked this conversation as resolved.
Outdated
|
||
|
|
||
| Das folgende Beispiel zeigt einen Resolver, der eine Buchliste mithilfe des `BookStoreService` bereitstellt: | ||
|
|
||
| ```typescript | ||
| import { inject } from '@angular/core'; | ||
| import { ResolveFn } from '@angular/router'; | ||
|
|
||
| export const booksResolver: ResolveFn<Book[]> = | ||
| (route, state) => { | ||
| const service = inject(BookStoreService); | ||
| return service.getAll(); | ||
| }; | ||
| ``` | ||
|
d-koppenhagen marked this conversation as resolved.
Outdated
|
||
|
|
||
| Um eine Resolver-Funktion zu generieren, können wir die Angular CLI nutzen: | ||
|
|
||
| ```bash | ||
| ng generate resolver books | ||
| ``` | ||
|
|
||
| ## Resolver in der Route registrieren | ||
|
|
||
| Damit der Resolver verwendet wird, muss er in der Routenkonfiguration registriert werden. | ||
| In der Eigenschaft `resolve` geben wir ein Objekt an, in dem alle Resolvers notiert sind. | ||
| Der Schlüssel in diesem Objekt (hier: `books`) ist frei wählbar. | ||
| Unter diesem Namen rufen wir die Daten anschließend in der Komponente ab. | ||
|
|
||
| ```typescript | ||
| { | ||
| path: 'mypath', | ||
| component: MyComponent, | ||
| resolve: { | ||
| books: booksResolver | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Sobald die Route aufgerufen wird, erstellt der Router automatisch eine Subscription auf das Observable, und die Daten werden abgerufen und gespeichert. | ||
| Erst dann wird die Komponente geladen. | ||
| Der Router wartet also, bis die asynchrone Operation abgeschlossen ist! | ||
|
|
||
| ## Auf die Daten in der Komponente zugreifen | ||
|
|
||
| Es gibt zwei Wege, um in der Komponente auf die aufgelösten Daten zuzugreifen. | ||
|
|
||
| ### Ansatz 1: ActivatedRoute | ||
|
|
||
| Die aufgelösten Daten sind über `ActivatedRoute` verfügbar. | ||
| Das Property `data` auf der Route enthält ein Observable, das die Daten aller Resolvers bereitstellt. | ||
| Die Schlüssel entsprechen den Namen, die wir im `resolve`-Objekt der Routenkonfiguration festgelegt haben. | ||
|
|
||
| ```typescript | ||
| import { Component, inject, computed } from '@angular/core'; | ||
| import { ActivatedRoute } from '@angular/router'; | ||
| import { toSignal } from '@angular/core/rxjs-interop'; | ||
|
|
||
| @Component({ /* ... */ }) | ||
| export class MyComponent { | ||
| private route = inject(ActivatedRoute); | ||
| private data = toSignal(this.route.data); | ||
|
|
||
| books = computed(() => this.data()?.books as Book[]); | ||
|
d-koppenhagen marked this conversation as resolved.
Outdated
|
||
| } | ||
| ``` | ||
|
|
||
| ### Ansatz 2: Component Input Binding (empfohlen) | ||
|
|
||
| Seit Angular 16 gibt es die Möglichkeit, aufgelöste Daten direkt als Inputs an die Komponente zu übergeben. | ||
| Dazu muss `withComponentInputBinding()` bei der Router-Konfiguration aktiviert sein: | ||
|
|
||
| ```typescript | ||
| import { provideRouter, withComponentInputBinding } from '@angular/router'; | ||
|
|
||
| bootstrapApplication(App, { | ||
| providers: [ | ||
| provideRouter(routes, withComponentInputBinding()) | ||
| ], | ||
| }); | ||
| ``` | ||
|
|
||
| In der Komponente definieren wir dann einen Input, dessen Name dem Schlüssel aus dem `resolve`-Objekt entspricht: | ||
|
|
||
| ```typescript | ||
| import { Component, input } from '@angular/core'; | ||
|
|
||
| @Component({ /* ... */ }) | ||
| export class MyComponent { | ||
| books = input.required<Book[]>(); | ||
| } | ||
| ``` | ||
|
|
||
| Dieser Ansatz ist typsicherer und eleganter, weil wir die `ActivatedRoute` nicht injizieren müssen. | ||
|
d-koppenhagen marked this conversation as resolved.
Outdated
|
||
|
|
||
| ## Fehlerbehandlung | ||
|
|
||
| Wenn ein Resolver fehlschlägt, wird standardmäßig ein `NavigationError` ausgelöst und die Navigation abgebrochen. | ||
| Das führt zu einer schlechten User Experience. | ||
| Deshalb sollten wir Fehler in Resolvers immer behandeln. | ||
|
|
||
| ### Fehler direkt im Resolver abfangen | ||
|
|
||
| Wir können im Resolver selbst mit `catchError` arbeiten und bei einem Fehler z. B. eine Weiterleitung auslösen: | ||
|
d-koppenhagen marked this conversation as resolved.
Outdated
|
||
|
|
||
| ```typescript | ||
| import { inject } from '@angular/core'; | ||
| import { ResolveFn, RedirectCommand, Router } from '@angular/router'; | ||
| import { catchError, of } from 'rxjs'; | ||
|
|
||
| export const booksResolver: ResolveFn<Book[] | RedirectCommand> = | ||
| (route, state) => { | ||
| const service = inject(BookStoreService); | ||
| const router = inject(Router); | ||
|
|
||
| return service.getAll().pipe( | ||
| catchError(error => { | ||
| console.error('Failed to load books:', error); | ||
| return of(new RedirectCommand(router.parseUrl('/error'))); | ||
| }) | ||
| ); | ||
| }; | ||
| ``` | ||
|
|
||
| ### Zentrale Fehlerbehandlung mit withNavigationErrorHandler | ||
|
d-koppenhagen marked this conversation as resolved.
Outdated
|
||
|
|
||
| Alternativ können wir einen zentralen Error-Handler für alle Navigationsfehler registrieren: | ||
|
|
||
| ```typescript | ||
| import { provideRouter, withNavigationErrorHandler } from '@angular/router'; | ||
|
|
||
| provideRouter( | ||
| routes, | ||
| withNavigationErrorHandler(error => { | ||
| const router = inject(Router); | ||
| console.error('Navigation error:', error.message); | ||
| router.navigate(['/error']); | ||
| }) | ||
| ); | ||
| ``` | ||
|
|
||
| ## Resolver für den Seitentitel | ||
|
|
||
| Im Kapitel zum Routing haben wir gezeigt, wie wir den Titel der Seite in der Routenkonfiguration mit dem Property `title` setzen können. | ||
| Dabei haben wir stets einen statischen Titel übergeben. | ||
|
|
||
| Wollen wir den Seitentitel hingegen dynamisch setzen, können wir einen Resolver verwenden. | ||
| Das zurückgegebene Observable muss dafür zu einem String auflösen. | ||
|
d-koppenhagen marked this conversation as resolved.
Outdated
|
||
| Zum Beispiel können wir den Buchtitel anhand der ISBN aus dem Routenparameter ermitteln: | ||
|
|
||
| ```typescript | ||
| import { inject } from '@angular/core'; | ||
| import { ResolveFn } from '@angular/router'; | ||
|
|
||
| export const bookTitleResolver: ResolveFn<string> = | ||
| (route, state) => { | ||
| const service = inject(BookStoreService); | ||
|
d-koppenhagen marked this conversation as resolved.
Outdated
|
||
|
|
||
| const isbn = route.paramMap.get('isbn')!; | ||
| return service.getTitleByISBN(isbn); | ||
| }; | ||
| ``` | ||
|
|
||
| Diesen Resolver geben wir dann in der Route im Property `title` an. | ||
| Das Observable wird automatisch vom Router aufgelöst, und der Seitentitel wird gesetzt: | ||
|
|
||
| ```typescript | ||
| { | ||
| path: 'books/:isbn', | ||
| component: BookDetailsComponent, | ||
|
d-koppenhagen marked this conversation as resolved.
Outdated
|
||
| title: bookTitleResolver | ||
| } | ||
| ``` | ||
|
|
||
| Dabei ist zu beachten, dass der Router auch hier auf das Ergebnis wartet, bevor die Komponente geladen wird. | ||
|
|
||
| ## Aufgelöste Daten in Kind-Resolvers verwenden | ||
|
|
||
| Resolvers werden von der Elternroute zur Kindroute ausgeführt. | ||
| Wenn eine Elternroute einen Resolver definiert, sind die aufgelösten Daten in Kind-Resolvers verfügbar: | ||
|
|
||
| ```typescript | ||
| provideRouter([ | ||
| { | ||
| path: 'users/:id', | ||
| resolve: { user: userResolver }, | ||
| children: [ | ||
| { | ||
| path: 'posts', | ||
| component: UserPosts, | ||
|
d-koppenhagen marked this conversation as resolved.
Outdated
|
||
| resolve: { | ||
| posts: (route: ActivatedRouteSnapshot) => { | ||
| const postService = inject(PostService); | ||
| const user = route.parent?.data['user'] as User; | ||
| return postService.getPostsByUser(user.id); | ||
| }, | ||
| }, | ||
| }, | ||
| ], | ||
| }, | ||
| ]); | ||
| ``` | ||
|
|
||
| ## Ladeindikator während der Navigation | ||
|
d-koppenhagen marked this conversation as resolved.
Outdated
|
||
|
|
||
| Da Resolvers die Navigation blockieren, kann es sinnvoll sein, einen globalen Ladeindikator anzuzeigen. | ||
| Dazu können wir den Navigationszustand des Routers überwachen: | ||
|
|
||
| ```typescript | ||
| import { Component, inject, computed } from '@angular/core'; | ||
| import { Router } from '@angular/router'; | ||
|
|
||
| @Component({ | ||
| selector: 'app-root', | ||
| template: ` | ||
| @if (isNavigating()) { | ||
| <div class="loading-bar">Laden...</div> | ||
|
d-koppenhagen marked this conversation as resolved.
Outdated
|
||
| } | ||
| <router-outlet /> | ||
| `, | ||
| }) | ||
| export class App { | ||
| private router = inject(Router); | ||
| isNavigating = computed(() => !!this.router.currentNavigation()); | ||
|
d-koppenhagen marked this conversation as resolved.
Outdated
|
||
| } | ||
| ``` | ||
|
|
||
| ## Best Practices | ||
|
|
||
| - **Resolvers sparsam verwenden:** Der Router wartet auf die asynchrone Operation und lädt die Komponente erst, wenn das Ergebnis vorliegt. Das widerspricht dem gewohnten Verhalten einer Single-Page-Anwendung, die schnell reagiert und die Daten zur Laufzeit nachlädt. | ||
| - **Keine regulären Nutzdaten laden:** HTTP-Requests können eine längere Zeit in Anspruch nehmen. Nutze stattdessen den herkömmlichen Weg und lade die Daten direkt in den Komponenten. | ||
| - **Fehler behandeln:** Fange Fehler im Resolver ab, um eine schlechte User Experience zu vermeiden. | ||
| - **Caching nutzen:** Speichere aufgelöste Daten zentral (z. B. in einem Service oder Store), damit sie nicht doppelt geladen werden müssen. | ||
| - **Ladeindikator anzeigen:** Da die Navigation blockiert wird, solltest du dem User visuelles Feedback geben. | ||
| - **Nur für besondere Fälle:** Resolvers sollten nur eingesetzt werden, wenn Daten unbedingt beim Start der Komponente benötigt werden und der Router aus gutem Grund den weiteren Ablauf verzögern soll. | ||
|
|
||
| ## Wann sind Resolvers sinnvoll? | ||
|
|
||
| Angesichts der beschriebenen UX-Probleme stellt sich die Frage: Gibt es überhaupt einen guten Anwendungsfall für Resolvers? | ||
|
|
||
| Ein valider Einsatz ist das Vorladen von Daten, die *sofort* verfügbar sind – z. B. gecachte Konfigurationsobjekte. | ||
| Wenn die Daten bereits im Speicher liegen und kein HTTP-Request mehr nötig ist, blockiert der Resolver die Navigation nicht spürbar. | ||
|
|
||
| Ein Beispiel: Wir haben einen `ConfigService`, der die Konfiguration beim Start der Anwendung einmalig lädt und anschließend aus dem Cache liefert: | ||
|
|
||
| ```typescript | ||
| import { Injectable, inject } from '@angular/core'; | ||
| import { HttpClient } from '@angular/common/http'; | ||
| import { shareReplay } from 'rxjs'; | ||
|
|
||
| @Injectable({ providedIn: 'root' }) | ||
|
d-koppenhagen marked this conversation as resolved.
|
||
| export class ConfigService { | ||
| private http = inject(HttpClient); | ||
|
|
||
| config$ = this.http.get<AppConfig>('/api/config').pipe( | ||
| shareReplay(1) | ||
| ); | ||
| } | ||
|
d-koppenhagen marked this conversation as resolved.
Outdated
|
||
| ``` | ||
|
|
||
| Der zugehörige Resolver gibt einfach das gecachte Observable zurück: | ||
|
|
||
| ```typescript | ||
| export const configResolver: ResolveFn<AppConfig> = | ||
| () => inject(ConfigService).config$; | ||
| ``` | ||
|
|
||
| Beim ersten Aufruf wird der HTTP-Request ausgeführt. | ||
| Bei allen weiteren Navigationen liefert `shareReplay` das Ergebnis sofort aus dem Cache – die Navigation wird also nicht verzögert. | ||
|
d-koppenhagen marked this conversation as resolved.
Outdated
|
||
|
|
||
| Aber auch hier gilt: Wir könnten den `ConfigService` genauso gut direkt in der Komponente injizieren und das `config$`-Observable dort nutzen. | ||
| Ein Resolver ist also selbst in diesem Fall nicht zwingend notwendig. | ||
|
|
||
| ## Empfehlung | ||
|
|
||
| Resolvers sind ein nützliches Werkzeug im Angular-Router, aber die Anwendungsfälle sind selten. | ||
| Für die allermeisten Szenarien ist der reaktive Ansatz – Daten direkt in der Komponente laden und mit `resource()`, `httpResource()` oder der `AsyncPipe` verarbeiten – die bessere Wahl. | ||
|
d-koppenhagen marked this conversation as resolved.
Outdated
|
||
|
|
||
| Wenn du Resolvers in deiner Codebasis hast und sie gut funktionieren: prima! | ||
| Wenn du überlegst, Resolvers neu einzuführen, prüfe zuerst, ob ein reaktiver Ansatz nicht einfacher und benutzerfreundlicher ist. | ||
|
|
||
| ## Zusammenfassung | ||
|
|
||
| - Ein Resolver ist eine Funktion vom Typ `ResolveFn<T>`, die ein Observable, eine Promise oder einen direkten Wert zurückgibt. | ||
| - Abhängigkeiten werden mit `inject()` angefordert. | ||
| - Der Resolver wird in der Routenkonfiguration unter `resolve` registriert. | ||
| - Die aufgelösten Daten können über `ActivatedRoute` oder über Component Input Binding (`withComponentInputBinding()`) abgerufen werden. | ||
| - Für den dynamischen Seitentitel kann ein Resolver im Property `title` der Route angegeben werden. | ||
| - Resolvers blockieren die Navigation. Deshalb sollten sie sparsam und nur für essenzielle Daten eingesetzt werden. | ||
| - Fehler in Resolvers sollten immer behandelt werden, z. B. mit `catchError` oder `withNavigationErrorHandler`. | ||
|
d-koppenhagen marked this conversation as resolved.
Outdated
|
||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.