Angular 19 introduces a convenient way to pass data to routed components using the routerOutletData input of the RouterOutlet directive. This feature simplifies data sharing between parent and child components within the routing hierarchy, enhancing component communication and reducing boilerplate code.
The Problem It Solves
Previously, passing data to routed components often involved complex workarounds, such as using route parameters, query parameters, shared services, router data, route resolver function, and state management libraries. These methods could become cumbersome, especially when dealing with complex data structures or nested routing scenarios. The routerOutletData input provides a more direct and intuitive way to pass data, improving code readability and maintainability.
Here are some standard methods and their drawbacks:
- Path Parameter: Requires the data to be part of the URL structure, making it less suitable for complex or non-identifying data.
- Query Parameter: Exposes data in the URL, which might not be desirable for sensitive information and can become unwieldy with large data sets.
- Services: Introduces a dependency on a shared service, potentially leading to increased complexity and a need for careful state management, especially in larger applications.
- Router Data: Primarily designed for static or route-specific configuration, not ideal for dynamic data passed during navigation.
- Router Function for Prefetching Data: While useful for loading data before navigation, it does not directly pass data from a parent component to its routed child in the same way as routerOutletData. A router function can return either a promise or an observable. If the function returns an observable, it is crucial that the observable completes. If it doesn't complete, the application can freeze, showing a loading screen, and preventing the user from navigating to the intended page.
- State Management Library (e.g., NgRx): It can be an overkill for simple data transfer between parent and child routes, adding significant complexity for a relatively narrow use case.
The routerOutletData input addresses these drawbacks by providing a straightforward, type-safe, efficient, and reactive way to pass data directly from a parent component to its routed child.
Example: Passing Data Through Nested Routes
Let's illustrate the usage of routerOutletData
with a practical example. We will create three standalone components: StarwarsListComponent
, StarWarsCharacterComponent
, and StarWarsMoviesComponent
. The StarWarsListComponent
component will pass data to the StarWarsCharacterComponent
, and the StarWarsCharacterComponent
will further pass data to the StarWarsMoviesComponent
, demonstrating data flow through nested routes.
1. Project Setup and Routing:
First, let’s define our routes in app.routes.ts
, starwars-list.routes.ts
, and starwars-character.routes.ts
.
// app.routes.ts
export const routes: Routes = [
{
path: 'starwars',
loadChildren: () => import('./starwars/routes/starwars-list.routes')
}
];
The routes array uses the loadChildern
property to lazy load the child routes in the starwars-list.routes.ts
file
// starwars-list.routes.ts
const starWarsListRoutes: Routes = [
{
path: '',
component: StarwarsListComponent,
children: [
{
path: ':id',
title: 'Star Wars Fighter',
loadChildren: () => import ('./starwars-character.routes'),
}
]
}
];
export default starWarsListRoutes;
When the route path is starwars
, the application navigates to the StarwarsListComponent
. When the route path is starwars/:id
, the child routes in the starwars-character.routes.ts
file are lazily loaded.
// starwars-character.routes.ts
const starWarsCharacterRoutes: Routes = [
{
path: '',
component: StarwarsCharacterComponent,
children: [
{
path: 'films',
loadComponent: () => import('../starwars-movies/starwars-movies.component'),
}
]
}
];
export default starWarsCharacterRoutes;
The application displays the StarwarsCharacterComponent
when the route path is starwars/:id
and lazily loads the StarwarsMoviesComponent
when the route path is starwars/:id/films
.
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
],
};
bootstrapApplication(App, appConfig);
The appConfig
provides the route configuration to the application and the bootStrapApplication
function bootstraps the App
component and the app configurations.
2. App Component (Great Grandparent):
export type FighterList = {
ids: number[];
isSith: boolean;
}
@Component({
selector: 'app-root',
imports: [RouterOutlet, FormsModule],
template: `
<div>
Select your allegiance:<br/>
<input type="radio" id="jedi" name="Jedi" value="jedi" [(ngModel)]="allegiance">
<label for="jedi">Jedi</label><br/>
<input type="radio" id="sith" name="Sith" value="sith" [(ngModel)]="allegiance">
<label for="sith">Sith</label><br/>
</div>
Selected allegiance: {{ allegiance() }}
<router-outlet [routerOutletData]="fighterIds()" />
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
allegiance = signal('jedi');
// When selecting the radio button, calculates the star wars character ids
fighterIds = computed<FighterList>(() => {
if (this.allegiance() == 'jedi') {
return { ids: [1, 10, 20, 51, 52, 53, 32], isSith: false };
}
return { ids: [4,44, 21, 67], isSith: true };
});
}
Explanation:
- In
App
component, when users change theallegiance
signal, thefighterIds
computed signal calculates the character ids. - The
routerOutletData
input of theRouterOutlet
is bound to thefighterIds
computed signal. The data is to be passed to theStarwarsListComponent
.
3. StarwarsListCharacter (Grandparent):
export type StarWarsCharacter = {
id: number;
name: string;
gender: string;
}
export type StarWarsCharacterNature = StarWarsCharacter & { isSith: boolean };
@Component({
selector: 'app-star-wars-card',
imports: [RouterLink],
template: `
<div class="card">
@let character = c();
<span>Id: {{ character.id }}</span>
<a [routerLink]="[character.id]" (click)="showFighter.emit(character)"
>{{ c().name }}</a>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class StarWarsCardComponent {
c = input.required<StarWarsCharacterNature>();
selectedFighter = signal<StarWarsCharacterNature | undefined>(undefined);
showFighter = output<StarWarsCharacterNature>();
}
@Component({
selector: 'app-starwars-list',
imports: [StarWarsCardComponent, RouterOutlet],
template: `
<div>
<h3 class="fighters">Fighters</h3>
@if (charactersResource.hasValue()) {
@for (c of charactersResource.value(); track c.id) {
<app-star-wars-card [c]="c"
(showFighter)="selectedFighter.set($event)" />
}
<router-outlet [routerOutletData]="selectedFighter()" />
}
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class StarwarsListComponent {
// inject the token to obtain the signal
fighterList = inject(ROUTER_OUTLET_DATA) as Signal<FighterList>;
starWarsCharactersService = inject(StarWarsCharactersService);
// Use the Resource API to retrieve the characters by ids
charactersResource = rxResource({
request: () => this.fighterList(),
loader: ({ request }) =>
this.starWarsCharactersService.retrieveCharacters(request),
defaultValue: [] as StarWarsCharacterNature[]
});
selectedFighter = signal<StarWarsCharacterNature | undefined>(undefined);
}
Explanation:
- In
StarwarsListComponent
, theROUTER_OUTLET_DATA
token is injected to retrieve thefighterList
from the App component. The injected value is cast to the expected typeSignal<FighterList>
. - The
characterResource
uses thefighterList
to call the Star Wars API to retrieve characters and display them in theStarWarsCardComponent
component. - Then, the
StarwarsListComponent
passes theselectedFighter
signal to theStarWarsCharacterComponent
.
4. StarwarsCharacterComponent (Parent):
@Component({
selector: 'app-starwars-character',
imports: [RouterLink, RouterOutlet],
template: `
<div>
@if (fighter(); as f) {
<div style="border: 1px solid black;">
<div class="character">
<p>Name: {{ f.name }}</p>
<p>Gender: {{ f.gender }}</p>
<p>Is Sith? {{ f.isSith }}</p>
</div>
<a [routerLink]="['films']" >Show Films</a>
</div>
<router-outlet [routerOutletData]="films()" />
}
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class StarwarsCharacterComponent {
fighter = inject(ROUTER_OUTLET_DATA) as Signal<StarWarsCharacterNature>;
films = computed(() => this.fighter().films);
}
Explanation:
- The
StarwarsCharacterComponent
component similarly injects theROUTER_OUTLET_DATA
token to access the Star Wars character from theStarwarsListComponent
component. - The
films
computed signal obtains the films array of the Star Wars character and passes it to therouterOutletData
of therouterOutlet
directive - If
StarwarsCharacterComponent
has a routed child component, it can inject the films for further processing.
5. StarwarsMoviesComponent (Child):
@Component({
selector: 'app-starwars-movies',
imports: [],
template: `
<div>
<h3 class="movies">Movies</h3>
@if (moviesResource.hasValue()) {
@for (c of moviesResource.value(); track c.title) {
<p>Title: {{ c.title }}</p>
<p>Episode: {{ c.episodeId }}</p>
<p>Release Date: {{ c.releaseDate }}</p>
<p>Opening Crawl: {{ c.openingCrawl }}</p>
<hr />
}
}
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class StarwarsMoviesComponent {
films = inject(ROUTER_OUTLET_DATA) as Signal<string[]>;
movieService = inject(StarWarsMoviesService);
moviesResource = rxResource({
request: () => this.films(),
loader: ({ request }) =>
this.movieService.retrieveMovies(request),
defaultValue: [] as StarWarsMovie[]
});
}
Explanation:
- The
StarwarsMoviesComponent
component similarly injects theROUTER_OUTLET_DATA
token to access the film URLs from theStarwarsCharacterComponent
component. - The
moviesResource
resource uses thefilms
signal to make HTTP requests to retrieve the film details. Then, the film details is displayed in the inline template.
Key Benefits
- Simplified Data Sharing: routerOutletData provides a direct and clean way to pass data between parent and child routed components.
- Improved Readability: Reduces boilerplate code and makes routing logic more understandable.
- Enhanced Maintainability: Centralizes data passing within the routing configuration.
- Type Safety: Using Signal and casting the injected data with the expected type, provides type safety.
- Component Flexibility: The parent component does not need to know about the details of the routed component. The parent component decides the data available to the routed component and passes the data to it. If the child needs the data, it will inject the
ROUTER_OUTLET_DATA
token. Otherwise, it ignores theroutedOutletData
input.
Conclusion
The routerOutletData
input in Angular 19 simplifies data sharing in routed components, offering a more efficient and maintainable approach. It is a valuable addition for developers working with complex routing scenarios.
Resources
- The PR of the feature: https://github.com/angular/angular/pull/57051
- The API documentation: https://angular.dev/api/router/ROUTER_OUTLET_DATA
- My Stackblitz Demo: https://stackblitz.com/edit/stackblitz-starters-lfhfttep?file=src%2Fmain.ts
Top comments (0)