Skip to content

Commit cfaf532

Browse files
Resolvers (#96)
* feat(material): Add resolvers article with routing data preloading guide - Add comprehensive guide on Angular Resolvers for preloading data before route activation - Document ResolveFn implementation with practical examples using BookStoreService - Explain resolver registration in route configuration with resolve property - Cover two approaches for accessing resolved data: ActivatedRoute and Component Input Binding - Include error handling patterns with catchError and withNavigationErrorHandler - Document dynamic page title resolution using resolvers - Add performance considerations and best practices for when to use resolvers - Provide complete code examples for common resolver patterns and use cases * feat(material resolvers): Add UX considerations and caching best practices - Add detailed section explaining UX problems with Resolvers blocking navigation - Document alternative approaches using resource() API and AsyncPipe for immediate component display - Include practical code examples for reactive data loading patterns - Add section on valid use cases for Resolvers with cached configuration example - Provide recommendation guidance on when to use Resolvers vs reactive approaches - Clarify that Resolvers should be reserved for special cases where data preloading is essential * Apply suggestions from code review Co-authored-by: Ferdinand Malcher <ferdinand@malcher.media> * docs(material resolvers): Update code examples to use modern Signal API - Replace AsyncPipe with `toSignal()` for Observable handling in component examples - Update template syntax to use Signal function calls instead of async pipe - Modernize ConfigService example to reflect current Angular patterns - Fix whitespace and formatting inconsistencies throughout the document - Align code examples with Signal-based reactive approach recommendations * docs(material resolvers): Clarify resolver return types and handling - Update explanation of resolver return value handling to be more precise - Document supported return types: T, Observable<T>, and Promise<T> - Add clarification that synchronous values can be returned directly - Explain use case for synchronous returns when reusing cached data - Improve wording around subscription and promise resolution behavior * docs(material resolvers): Clarify type safety limitations of resolver inputs - Remove claim that resolver input approach is type-safe - Add explanation that compiler does not validate input type matches resolver return type - Clarify that type mismatches are only caught at runtime - Emphasize that resolver inputs accept any type without compile-time validation * docs(material resolvers): Reorganize content and update code examples - Move UX problem section after resolver definition for better flow - Remove outdated best practices section and integrate guidance into narrative - Update code examples to use modern Signal API and consistent service naming - Simplify resolver return type explanation by removing redundant details - Change private field syntax from `#router` to `private router` for consistency - Update `BookStoreService` references to `BookStore` throughout examples - Improve narrative structure to emphasize when resolvers are appropriate vs. alternatives * Update material/resolvers/README.md Co-authored-by: Danny Koppenhagen <mail@d-koppenhagen.de> * docs(material resolvers): Refine code style and clarify data access patterns Switch examples to modern Angular conventions (`#` private fields, `protected readonly`), restructure with a dedicated "Resolver anlegen" section, and simplify the `ActivatedRoute` example via `snapshot.data`. * date --------- Co-authored-by: Ferdinand Malcher <ferdinand@malcher.media>
1 parent b3dc898 commit cfaf532

1 file changed

Lines changed: 385 additions & 0 deletions

File tree

material/resolvers/README.md

Lines changed: 385 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,385 @@
1+
---
2+
title: 'Resolvers: Daten beim Routing vorladen'
3+
published: 2026-05-17
4+
lastModified: 2026-05-17
5+
---
6+
7+
Der Angular-Router bietet mit *Resolvers* die Möglichkeit, Daten asynchron vorzuladen, bevor eine Komponente aktiviert wird.
8+
Hier betrachten wir die Funktionsweise, Implementierung und den Zugriff auf die aufgelösten Daten in der Komponente.
9+
Außerdem diskutieren wir, wann Resolvers sinnvoll sind – und wann nicht.
10+
11+
## Inhalt
12+
13+
[[toc]]
14+
15+
## Motivation
16+
17+
Asynchrone Operationen wie HTTP-Requests lösen wir in Angular üblicherweise direkt in der Komponente auf.
18+
Dafür gibt es verschiedene Möglichkeiten:
19+
Wir können die moderne Resource API einsetzen oder den `HttpClient` direkt verwenden und subscriben.
20+
Die Funktion `toSignal()` und die AsyncPipe helfen uns bei der Arbeit mit Observables.
21+
Die Komponente ist dabei sofort sichtbar, und wir können einen Ladeindikator anzeigen, während die Daten eintreffen.
22+
23+
Als Alternative bietet der Router sogenannte *Resolvers* an, um Daten schon vor dem Start der Komponente asynchron vorzuladen.
24+
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.
25+
26+
## Einen Resolver definieren
27+
28+
Ein Resolver wird als Funktion mit dem Typ `ResolveFn<T>` definiert.
29+
Der generische Typparameter `T` gibt an, welchen Datentyp das Ergebnis besitzt.
30+
31+
Als Argumente erhält die Funktion die aktuelle Route in Form eines `ActivatedRouteSnapshot` und den Zustand des Routers als `RouterStateSnapshot`.
32+
Wir können also z. B. Routenparameter auslesen und diese bei der Datenabfrage verarbeiten.
33+
34+
Alle Abhängigkeiten fordern wir mithilfe von `inject()` an.
35+
Den Rückgabewert geben wir direkt aus der Funktion zurück – um die Subscription bzw. das Auflösen kümmert sich der Router automatisch.
36+
37+
Der Rückgabetyp der Funktion ist `T | Observable<T> | Promise<T>`.
38+
Wir können also ein Observable zurückgeben, eine Promise (z. B. wenn wir einen bestehenden Promise-basierten Loader wiederverwenden) oder auch einen synchronen Wert.
39+
Letzteres ist besonders interessant, wenn wir z. B. bereits geladene Daten aus einem Service direkt verteilen wollen, ohne eine asynchrone Operation auszulösen.
40+
41+
Das folgende Beispiel zeigt einen Resolver, der eine Buchliste mithilfe des `BookStore` bereitstellt:
42+
43+
```typescript
44+
import { inject } from '@angular/core';
45+
import { ResolveFn } from '@angular/router';
46+
47+
export const booksResolver: ResolveFn<Book[]> =
48+
(route, state) => {
49+
const store = inject(BookStore);
50+
return store.getAll();
51+
};
52+
```
53+
54+
Geben wir aus dem Resolver ein Observable zurück, wird der Datenstrom automatisch subscribt, und der Resolver kümmert sich um die Auflösung der asynchronen Operation.
55+
Wichtig ist aber, dass ein Resolver immer nur zu *genau einem* Ergebnis auflöst.
56+
Bei einem Observable mit mehreren Elementen wird nur das erste Ergebnis ausgegeben.
57+
58+
59+
## Resolver anlegen
60+
61+
Um eine Resolver-Funktion zu generieren, können wir die Angular CLI nutzen:
62+
63+
```bash
64+
ng generate resolver books
65+
```
66+
67+
## Resolver in der Route registrieren
68+
69+
Damit der Resolver verwendet wird, muss er in der Routenkonfiguration registriert werden.
70+
In der Eigenschaft `resolve` geben wir ein Objekt an, in dem alle Resolvers notiert sind.
71+
Der Schlüssel in diesem Objekt (hier: `books`) ist frei wählbar.
72+
Unter diesem Namen rufen wir die Daten anschließend in der Komponente ab.
73+
74+
```typescript
75+
{
76+
path: 'books',
77+
component: DashboardPage,
78+
resolve: {
79+
books: booksResolver
80+
}
81+
}
82+
```
83+
84+
Sobald die Route aufgerufen wird, wird der Resolver ausgeführt, und die Daten werden in den Metadaten der Route gespeichert.
85+
Erst danach wird die Komponente geladen, und die Daten stehen sofort beim Start der Komponente bereit.
86+
Bei asynchronen Operationen ist hier aber besondere Vorsicht geboten: Der Router wartet, bis die Operation abgeschlossen ist! Der tatsächliche Routenwechsel wird also durch den Resolver verzögert.
87+
88+
## Daten auslesen
89+
90+
Nachdem Daten durch Resolver bereitgestellt wurden, gibt es zwei Wege, um in der Komponente damit zu arbeiten.
91+
Grundlegend werden die Daten über den gleichen Kanal bereitgestellt wie statische [`data`, die wir direkt in der Route notieren](https://angular.dev/api/router/Route#data).
92+
93+
### Ansatz 1: ActivatedRoute
94+
95+
Die aufgelösten Daten sind über den Service `ActivatedRoute` verfügbar.
96+
Das Objekt liefert die Daten wahlweise als Observable `data` oder synchron über den Snapshot in `snapshot.data`.
97+
Da ein Resolver immer nur ein einziges Ergebnis liefert, ist der reaktive Weg hier nicht notwendig, und wir können die bereitgestellten Daten direkt aus dem Snapshot lesen.
98+
99+
Die Schlüssel im Objekt entsprechen den Namen, die wir im `resolve`-Objekt der Routenkonfiguration festgelegt haben.
100+
101+
```typescript
102+
import { Component, inject } from '@angular/core';
103+
import { ActivatedRoute } from '@angular/router';
104+
105+
@Component({ /* ... */ })
106+
export class DashboardPage {
107+
#route = inject(ActivatedRoute);
108+
protected books = this.#route.snapshot.data['books'] as Book[];
109+
}
110+
```
111+
112+
### Ansatz 2: Component Input Binding (empfohlen)
113+
114+
Mit dem Component Input Binding können wir Routenparameter und Data als Inputs in gerouteten Komponenten empfangen.
115+
Dazu muss `withComponentInputBinding()` bei der Router-Konfiguration aktiviert sein:
116+
117+
```typescript
118+
import { provideRouter, withComponentInputBinding } from '@angular/router';
119+
120+
bootstrapApplication(App, {
121+
providers: [
122+
provideRouter(routes, withComponentInputBinding())
123+
],
124+
});
125+
```
126+
127+
In der Komponente definieren wir dann einen Input, dessen Name dem Schlüssel aus dem `resolve`-Objekt entspricht:
128+
129+
```typescript
130+
import { Component, input } from '@angular/core';
131+
132+
@Component({ /* ... */ })
133+
export class DashboardPage {
134+
readonly books = input.required<Book[]>();
135+
}
136+
```
137+
138+
Beim Start der Komponente werden die geladenen Daten sofort im Input zur Verfügung gestellt.
139+
Dieser Ansatz ist eleganter, weil wir die `ActivatedRoute` nicht injizieren müssen.
140+
Außerdem ist diese Komponente einfacher zu testen: Anstatt einen Service auszumocken, müssen wir lediglich in der Testumgebung die benötigten Daten per Input bereitstellen.
141+
142+
## Fehlerbehandlung
143+
144+
Wenn ein Resolver fehlschlägt, wird standardmäßig ein `NavigationError` ausgelöst und die Navigation abgebrochen.
145+
Das führt zu einer schlechten User Experience.
146+
Deshalb sollten wir Fehler in Resolvers immer behandeln.
147+
148+
### Fehler direkt im Resolver abfangen
149+
150+
Wenn der Resolver mit Observables arbeitet, können wir mit dem RxJS-Operator `catchError()` arbeiten und bei einem Fehler z. B. eine Weiterleitung auslösen.
151+
Dazu kann der Resolver ein `RedirectCommand` zurückgeben.
152+
153+
```typescript
154+
import { inject } from '@angular/core';
155+
import { ResolveFn, RedirectCommand, Router } from '@angular/router';
156+
import { catchError, of } from 'rxjs';
157+
158+
export const booksResolver: ResolveFn<Book[] | RedirectCommand> =
159+
(route, state) => {
160+
const store = inject(BookStore);
161+
const router = inject(Router);
162+
163+
return store.getAll().pipe(
164+
catchError(error => {
165+
console.error('Failed to load books:', error);
166+
return of(new RedirectCommand(router.parseUrl('/error')));
167+
})
168+
);
169+
};
170+
```
171+
172+
### Zentrale Fehlerbehandlung mit `withNavigationErrorHandler()`
173+
174+
Alternativ können wir einen zentralen Error-Handler für alle Navigationsfehler registrieren:
175+
176+
```typescript
177+
import { provideRouter, withNavigationErrorHandler } from '@angular/router';
178+
179+
provideRouter(
180+
routes,
181+
withNavigationErrorHandler(error => {
182+
const router = inject(Router);
183+
console.error('Navigation error:', error.message);
184+
router.navigate(['/error']);
185+
})
186+
);
187+
```
188+
189+
## Resolver für den Seitentitel
190+
191+
Im Kapitel zum Routing haben wir gezeigt, wie wir den Titel der Seite in der Routenkonfiguration mit dem Property `title` setzen können.
192+
Dabei haben wir stets einen statischen Titel übergeben.
193+
194+
Wollen wir den Seitentitel hingegen dynamisch setzen, können wir einen Resolver verwenden.
195+
Der Resolver muss dafür zu einem String auflösen.
196+
Zum Beispiel können wir den Buchtitel anhand der ISBN aus dem Routenparameter ermitteln:
197+
198+
```typescript
199+
import { inject } from '@angular/core';
200+
import { ResolveFn } from '@angular/router';
201+
202+
export const bookTitleResolver: ResolveFn<string> =
203+
(route, state) => {
204+
const store = inject(BookStore);
205+
206+
const isbn = route.paramMap.get('isbn')!;
207+
return store.getTitleByISBN(isbn);
208+
};
209+
```
210+
211+
Diesen Resolver geben wir dann in der Route im Property `title` an.
212+
Das Observable wird automatisch vom Router aufgelöst, und der Seitentitel wird gesetzt:
213+
214+
```typescript
215+
{
216+
path: 'books/:isbn',
217+
component: BookDetailsPage,
218+
title: bookTitleResolver
219+
}
220+
```
221+
222+
Dabei ist zu beachten, dass der Router auch hier auf das Ergebnis wartet, bevor die Komponente geladen wird.
223+
224+
## Aufgelöste Daten in Kind-Resolvers verwenden
225+
226+
Resolvers werden von der Elternroute zur Kindroute ausgeführt.
227+
Wenn eine Elternroute einen Resolver definiert, sind die aufgelösten Daten in Kind-Resolvers verfügbar:
228+
229+
```typescript
230+
export const routes: Routes = [
231+
{
232+
path: 'users/:id',
233+
resolve: { user: userResolver },
234+
children: [
235+
{
236+
path: 'posts',
237+
component: UserPostsPage,
238+
resolve: {
239+
posts: (route: ActivatedRouteSnapshot) => {
240+
const postService = inject(PostService);
241+
const user = route.parent?.data['user'] as User;
242+
return postService.getPostsByUser(user.id);
243+
},
244+
},
245+
},
246+
],
247+
},
248+
];
249+
```
250+
251+
Übrigens: In diesem Codebeispiel haben wir den Resolver *inline* direkt in der Route definiert.
252+
Üblicherweise wird ein Resolver in einer separaten Funktion notiert.
253+
Für sehr kurze Logik, die nur einmalig verwendet wird, kann hingegen die hier gezeigte Schreibweise sinnvoll sein.
254+
255+
256+
## Ladeindikator während der Navigation anzeigen
257+
258+
Da Resolvers bei asynchronen Operationen die Navigation blockieren, kann es sinnvoll sein, einen globalen Ladeindikator anzuzeigen.
259+
Dazu können wir den Navigationszustand des Routers überwachen.
260+
Das Signal `currentNavigation` enthält immer Informationen über die laufende Navigation – oder `null`, wenn der Router nicht navigiert.
261+
262+
```typescript
263+
import { Component, inject, computed } from '@angular/core';
264+
import { Router } from '@angular/router';
265+
266+
@Component({
267+
selector: 'app-root',
268+
template: `
269+
@if (isNavigating()) {
270+
<div class="loading-bar">Wird geladen …</div>
271+
}
272+
<router-outlet />
273+
`,
274+
})
275+
export class App {
276+
#router = inject(Router);
277+
protected readonly isNavigating = computed(() => !!this.#router.currentNavigation());
278+
}
279+
```
280+
281+
## Das UX-Problem mit Resolvers
282+
283+
Resolvers sind einfach zu verwenden – aber sie haben ein grundlegendes Problem:
284+
Der Router wartet auf die asynchrone Operation, bevor die Route aktiviert wird.
285+
286+
Stell dir einen langsamen HTTP-Request vor.
287+
Nach dem Klick auf einen Link startet der Request, aber die Navigation wird erst abgeschlossen, wenn die Antwort eintrifft.
288+
Dauert der Request 5 Sekunden, dauert auch die Navigation 5 Sekunden.
289+
In dieser Zeit sieht der User keine Reaktion – und klickt womöglich mehrfach auf den Link.
290+
291+
Dieses Verhalten widerspricht der Grundidee einer Single-Page-Anwendung:
292+
Eine SPA sollte immer schnell reagieren und die Daten zur Laufzeit nachladen.
293+
Mit asynchronen Operationen in Resolvers kehren wir zum Verhalten einer klassischen serverseitig gerenderten Seite zurück: Klick, warten, weiter.
294+
295+
### Die Alternative: Daten direkt in der Komponente laden
296+
297+
Ohne Resolvers wird die Komponente sofort angezeigt, und die Daten werden im Hintergrund geladen.
298+
Während der Ladezeit können wir einen Ladeindikator oder Platzhalter-Elemente (Ghost Elements) anzeigen.
299+
Die Navigation ist sofort abgeschlossen, und der User erhält unmittelbares Feedback.
300+
301+
Mit der Resource API geht das besonders elegant:
302+
303+
```typescript
304+
@Component({ /* ... */ })
305+
export class DashboardPage {
306+
#store = inject(BookStore);
307+
booksResource = this.#store.getAllAsResource();
308+
}
309+
```
310+
311+
Alternativ können wir Observables mit `toSignal()` in ein Signal umwandeln und im Template nutzen:
312+
313+
```typescript
314+
@Component({
315+
template: `
316+
@if (books().length) {
317+
<app-book-list [books]="books()" />
318+
} @else {
319+
<p>Laden …</p>
320+
}
321+
`,
322+
})
323+
export class DashboardPage {
324+
protected readonly books = toSignal(inject(BookStore).getAll(), { initialValue: [] });
325+
}
326+
```
327+
328+
In beiden Fällen ist die Komponente sofort sichtbar, und die Daten werden asynchron nachgeladen.
329+
Das ist häufig die bessere Wahl gegenüber einem Resolver.
330+
331+
332+
## Wann sind Resolvers sinnvoll?
333+
334+
Angesichts der beschriebenen UX-Probleme stellt sich die Frage: Gibt es überhaupt einen guten Anwendungsfall für Resolvers?
335+
336+
Ein valider Einsatz ist das Vorladen von Daten, die *sofort* verfügbar sind – z. B. gecachte Konfigurationsobjekte.
337+
Wenn die Daten bereits im Speicher liegen und kein HTTP-Request mehr nötig ist, blockiert der Resolver die Navigation nicht spürbar.
338+
339+
Ein Beispiel: Wir haben einen `ConfigService`, der die Konfiguration beim Start der Anwendung einmalig lädt und anschließend aus dem Cache liefert:
340+
341+
```typescript
342+
import { Injectable, inject } from '@angular/core';
343+
import { HttpClient } from '@angular/common/http';
344+
import { shareReplay } from 'rxjs';
345+
346+
@Injectable({ providedIn: 'root' })
347+
export class ConfigService {
348+
#http = inject(HttpClient);
349+
350+
readonly config$ = this.#http.get<AppConfig>('/api/config').pipe(
351+
shareReplay(1)
352+
);
353+
}
354+
```
355+
356+
Der zugehörige Resolver gibt einfach das gecachte Observable zurück:
357+
358+
```typescript
359+
export const configResolver: ResolveFn<AppConfig> =
360+
() => inject(ConfigService).config$;
361+
```
362+
363+
Beim ersten Aufruf wird der HTTP-Request ausgeführt.
364+
Bei allen weiteren Navigationen liefert der Operator `shareReplay()` das Ergebnis sofort aus dem Cache – die Navigation wird also nicht verzögert.
365+
366+
Aber auch hier gilt: Wir könnten den `ConfigService` genauso gut direkt in der Komponente injizieren und das Observable `config$` dort nutzen.
367+
Ein Resolver ist also selbst in diesem Fall nicht zwingend notwendig.
368+
369+
## Empfehlung
370+
371+
Resolvers sind ein nützliches Werkzeug im Angular-Router, aber die Anwendungsfälle sind selten.
372+
Für die allermeisten Szenarien ist der reaktive Ansatz – Daten direkt in der Komponente laden und mit `resource()`, `httpResource()` oder `toSignal()` verarbeiten – die bessere Wahl.
373+
374+
Wenn du Resolvers in deiner Codebasis hast und sie gut funktionieren: prima!
375+
Wenn du überlegst, Resolvers neu einzuführen, prüfe zuerst, ob ein reaktiver Ansatz nicht einfacher und benutzerfreundlicher ist.
376+
377+
## Zusammenfassung
378+
379+
- Ein Resolver ist eine Funktion vom Typ `ResolveFn<T>`, die ein Observable, eine Promise oder einen direkten Wert zurückgibt.
380+
- Abhängigkeiten werden mit `inject()` angefordert.
381+
- Der Resolver wird in der Routenkonfiguration unter `resolve` registriert.
382+
- Die aufgelösten Daten können über `ActivatedRoute` oder über Component Input Binding (`withComponentInputBinding()`) abgerufen werden.
383+
- Für den dynamischen Seitentitel kann ein Resolver im Property `title` der Route angegeben werden.
384+
- Resolvers blockieren die Navigation. Deshalb sollten sie sparsam und nur für essenzielle Daten eingesetzt werden.
385+
- Fehler in Resolvers sollten immer behandelt werden, z. B. mit `catchError()` oder `withNavigationErrorHandler()`.

0 commit comments

Comments
 (0)