Passing Data to Routed Components with RouterOutletData in Angular 19

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: ` Select your allegiance: Jedi Sith Selected allegiance: {{ allegiance() }}

Mar 27, 2025 - 03:06
 0
Passing Data to Routed Components with RouterOutletData in Angular 19

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: `
     
Select your allegiance:


Selected allegiance: {{ allegiance() }} `
, 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 the allegiance signal, the fighterIds computed signal calculates the character ids.
  • The routerOutletData input of the RouterOutlet is bound to the fighterIds computed signal. The data is to be passed to the StarwarsListComponent.

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: `
   
@let character = c(); Id: {{ character.id }} {{ c().name }}
`
, 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: `
   

Fighters

@if (charactersResource.hasValue()) { @for (c of charactersResource.value(); track c.id) { } }
`
, 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, the ROUTER_OUTLET_DATA token is injected to retrieve the fighterList from the App component. The injected value is cast to the expected type Signal.
  • The characterResource uses the fighterList to call the Star Wars API to retrieve characters and display them in the StarWarsCardComponent component.
  • Then, the StarwarsListComponent passes the selectedFighter signal to the StarWarsCharacterComponent.

4. StarwarsCharacterComponent (Parent):

@Component({
 selector: 'app-starwars-character',
 imports: [RouterLink, RouterOutlet],
 template: `
   
@if (fighter(); as f) {

Name: {{ f.name }}

Gender: {{ f.gender }}

Is Sith? {{ f.isSith }}

Show Films
}
`
, 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 the ROUTER_OUTLET_DATA token to access the Star Wars character from the StarwarsListComponent component.
  • The films computed signal obtains the films array of the Star Wars character and passes it to the routerOutletData of the routerOutlet 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: `
   

Movies

@if (moviesResource.hasValue()) { @for (c of moviesResource.value(); track c.title) {

Title: {{ c.title }}

Episode: {{ c.episodeId }}

Release Date: {{ c.releaseDate }}

Opening Crawl: {{ c.openingCrawl }}


} }
`
, 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 the ROUTER_OUTLET_DATA token to access the film URLs from the StarwarsCharacterComponent component.
  • The moviesResource resource uses the films 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 the routedOutletData 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