|
| 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