skip to content

In Angular, what do a router-outlet's activate and deactivate outputs emit, and when would you pass routerOutletData instead?

level: middleimportance: nice to knowfreq 28%

answer

  1. the component instance
  2. created and destroyed
  3. attach and detach for reuse
  4. ROUTER_OUTLET_DATA signal

basics

~20 s

activate emits the routed component instance when the outlet creates it, and deactivate emits it when the outlet destroys it. To send context into routed components, bind routerOutletData and inject ROUTER_OUTLET_DATA as a signal instead of poking the instance.

solid answer

~40 s

`RouterOutlet` has four outputs: `activate` (a routed component was created), `deactivate` (it was destroyed), and `attach`/`detach`, which fire only when a `RouteReuseStrategy` stores and restores a view. `activate` and `deactivate` emit the **component instance**, so a host can react to which view is shown: start or stop a timer, move focus, update a toolbar. The template reference `#outlet="outlet"` also exposes `isActivated`, `component`, `activatedRoute` and `activatedRouteData`. Casting the emitted instance and setting fields couples the host to every child type, and the fields are not inputs. For passing context down, v19 added the `routerOutletData` input: `<router-outlet [routerOutletData]="{density: 'compact'}" />`, read in the routed component, and its template children, with `inject(ROUTER_OUTLET_DATA)`, which returns a signal that updates when the binding changes.

code

ts · 25 lines
ts
import {afterNextRender, Component, ElementRef, inject, Injector, signal, Signal} from '@angular/core';
import {ROUTER_OUTLET_DATA, RouterOutlet} from '@angular/router';

@Component({
  selector: 'app-mail-shell',
  imports: [RouterOutlet],
  template: `
    <router-outlet (activate)="focusMain()" />
    <aside><router-outlet name="side" [routerOutletData]="{density: density()}" /></aside>
  `,
})
export class MailShell {
  private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);
  private readonly injector = inject(Injector);
  readonly density = signal<'compact' | 'comfortable'>('compact');
  focusMain(): void {
    // activate fires before the new view has rendered: wait, then focus its heading
    afterNextRender(() => this.host.nativeElement.querySelector<HTMLElement>('h1')?.focus(), {injector: this.injector});
  }
}

@Component({selector: 'app-compose', template: `<form [class.compact]="data().density === 'compact'"></form>`})
export class Compose {
  readonly data = inject(ROUTER_OUTLET_DATA) as Signal<{density: string}>;
}

go deeper

for a junior

Know that router-outlet has activate and deactivate events that hand you the component instance being shown or removed.

for a middle

Explain which outputs fire with the default reuse strategy, what the outlet reference exposes, and how routerOutletData reaches routed components.

for a senior

Use the events for focus and host chrome, avoid instance poking, and pick routerOutletData, URL state or a service for each kind of context.

for a principal

Set a rule for how layouts pass context into routed views so features stay decoupled from the shells that host them.

## The outlet's outputs `<router-outlet>` is the `RouterOutlet` directive from `@angular/router`. Besides rendering routed components, it reports what it does: | Output | Fires when | Emits | |---|---|---| | `activate` | the outlet creates a routed component | the component instance | | `deactivate` | the outlet destroys it | the component instance | | `attach` | a `RouteReuseStrategy` re-inserts a stored view | the component instance | | `detach` | a `RouteReuseStrategy` removes a view to store it | the component instance | With the default reuse strategy nothing is stored, so only `activate` and `deactivate` fire. A navigation that **reuses** the component, such as `/mail/1` to `/mail/2` on the same route config, fires neither, because nothing is created or destroyed. ## Reading the outlet from the host The directive is exported as `outlet`: ```html <router-outlet #outlet="outlet" (activate)="onShown($event)" (deactivate)="onHidden($event)" /> @if (outlet.isActivated) { <p>Showing {{ outlet.activatedRouteData['label'] }}</p> } ``` - `isActivated` - whether a component is currently shown; - `component` and `activatedRoute` - throw when nothing is activated, so guard with `isActivated`; - `activatedRouteData` - the snapshot's data, or `{}` when inactive. ## Good uses for the events - **Focus management**: after `activate`, move focus to the new view's heading so keyboard and screen-reader users land in the right place. - **Host chrome**: show or hide a toolbar depending on which view type was activated. - **Cleanup hooks** in the host, such as stopping a polling timer when the side panel's component is destroyed. ## The coupling trap A frequent anti-pattern is `onActivate(cmp: any) { cmp.folder = this.folder; }`. It: 1. couples the host to the concrete fields of every routed component; 2. bypasses inputs, so signal-based components never see a change notification; 3. runs only on creation, so it silently stops updating when the component is reused. ## `routerOutletData`: context without coupling Angular 19 added an input on the outlet for exactly this: ```html <router-outlet [routerOutletData]="{density: density()}" /> ``` The routed component, and anything in its template, injects it: ```ts readonly outletData = inject(ROUTER_OUTLET_DATA) as Signal<{density: string}>; ``` The token provides a **signal** of the input's value, so when the host changes `density()` the routed component's `computed()`s and template follow. It is per outlet, so a primary outlet and a `side` outlet can pass different context to whatever they render, with no change to route definitions. ## Where the outputs fire relative to change detection `activate` is emitted right after the outlet creates the component and inserts it, before the new view's first check has necessarily run, so code that needs the rendered DOM, such as focusing an element inside the view, should wait for rendering, for example with `afterNextRender()`. `deactivate` is emitted after the component is destroyed, so the instance it carries should be treated as read-only history, not something to call methods on. ## Choosing a channel - Outlet-specific presentation context (density, layout, which panel you are in): `routerOutletData`. - Values that belong in the URL (ids, filters): route params, query params or route `data`. - Shared application state: an injectable service. - Reacting in the host to what is shown: `activate` and `deactivate`.

  • Why does activate not fire when navigating from /mail/1 to /mail/2?
    Both URLs match the same route config, so the router reuses the existing component and only updates its ActivatedRoute. Nothing is created or destroyed, so neither activate nor deactivate is emitted.
  • When do attach and detach fire?
    Only when a custom RouteReuseStrategy tells the router to store a view on leaving a route and re-insert it later. The outlet then emits detach when the view is removed and attach when it returns, instead of destroying and recreating it.

saying these in an interview costs you the question

  • Setting fields on the emitted instance to pass data into the routed component
  • Expecting activate to fire when only route params change on a reused component
  • Reading outlet.component without checking isActivated first
  • Believing attach and detach fire on every navigation
  • Thinking routerOutletData requires changing the route definitions