With Angular's NgTemplateOutlet, what happens to the rendered view when the context object changes versus when the template or injector input changes?
answer
- one input rebuilds, one does not
- a Proxy stands in for context
- state loss inside the outlet
- stable references for template and injector
basics
~20 sA new template or injector makes NgTemplateOutlet destroy the embedded view and create a new one, losing child state. A new context object keeps the view: a Proxy forwards reads to whatever object is currently bound.
solid answer
~30 s`NgTemplateOutlet` only recreates its embedded view when `ngTemplateOutlet` or `ngTemplateOutletInjector` changes: it removes the old view, which destroys its components, and calls `createEmbeddedView()` again, or renders nothing if the template is now null. The context is different: the view is rendered with a `Proxy` that forwards reads and writes to the current `ngTemplateOutletContext`, so passing a new object literal every check is cheap and keeps component state. The trap is binding the template or injector to something that returns a new reference each time, such as `[ngTemplateOutletInjector]="makeInjector()"`, which rebuilds the view on every pass and resets any state inside it.
code
ts · 22 linesimport { Component, Injector, inject, signal } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
@Component({
selector: 'app-panel-host',
imports: [NgTemplateOutlet],
template: `
<ng-template #panel let-title>
<input [placeholder]="title" />
</ng-template>
<!-- Kept: a new context literal each check does not recreate the view -->
<ng-container [ngTemplateOutlet]="panel"
[ngTemplateOutletContext]="{ $implicit: title() }"
[ngTemplateOutletInjector]="panelInjector" />
`,
})
export class PanelHost {
title = signal('Search');
// Created once: a fresh Injector per check would rebuild the view every time
panelInjector = Injector.create({ providers: [], parent: inject(Injector) });
}go deeper
Recall that the outlet has a template input and a context input, and that changing the template swaps the rendered content.
Explain which inputs rebuild the embedded view and why a new context object alone does not.
Diagnose unexpected resets inside an outlet by finding an input bound to a fresh reference each check, then hoist it to a stable field.
Weigh template swapping against context-driven variation when designing reusable components whose inner state must survive updates.
## What `NgTemplateOutlet` actually keeps `NgTemplateOutlet` renders a `TemplateRef` into the view container at its own location, creating an **embedded view**. It has three inputs: `ngTemplateOutlet` (the template), `ngTemplateOutletContext` (the context object) and `ngTemplateOutletInjector` (an optional injector). In Angular 22 the directive treats them very differently when they change. ## Changing the template or the injector: the view is rebuilt When `ngTemplateOutlet` or `ngTemplateOutletInjector` receives a new value, the directive's `ngOnChanges` runs this sequence: 1. remove the existing embedded view from the container, which **destroys** it and every component and directive inside it; 2. if the new template is `null` or `undefined`, stop there and render nothing; 3. otherwise call `createEmbeddedView()` with the new template and a fresh context wrapper. Destroying the view has visible consequences: child components are constructed again, `ngOnInit` runs again, local state such as a half-typed input value or an expanded panel is lost, and focus leaves the element. ## Changing the context: the view is kept Changes to `ngTemplateOutletContext` do **not** recreate the view. The directive renders the template with a **`Proxy`** as its context, and that proxy forwards every property read and write to whatever object is *currently* bound to `ngTemplateOutletContext`. So: - passing a **new object literal** on every change detection pass is cheap; the existing view stays and simply reads the new values on its next check; - mutating a key on the same object is also picked up on the next check; - binding the context to `null` makes every context read return `undefined` rather than tearing the view down. Angular 17 dropped support for swapping the whole context object of an `EmbeddedViewRef`, and its changelog points to `NgTemplateOutlet`'s proxy as the pattern to copy if you still need that behaviour. | Input that changes | What happens to the rendered view | State inside it | |---|---|---| | `ngTemplateOutlet` | Destroyed and recreated | Lost | | `ngTemplateOutletInjector` | Destroyed and recreated | Lost | | `ngTemplateOutletContext` | Kept, reads new values | Kept | | Template set to `null` | Destroyed, nothing rendered | Lost | ## Why this matters in real code The classic symptom is a component inside an outlet that "resets" unexpectedly. Common causes: - **Switching templates on purpose.** A `computed` or getter picking between two `TemplateRef`s returns stable references, so it only rebuilds when the choice actually flips; but each flip destroys the state inside the old view by design. - **An injector created in a getter or in the template.** Something like `[ngTemplateOutletInjector]="makeInjector()"` produces a new `Injector` on every check, so the view is destroyed and rebuilt on every change detection pass. Create the injector once, as a field, and reuse it. - **Expecting a context change to reset things.** Because the view is kept, a child component inside the fragment will not re-run `ngOnInit` when the row in the context changes. It must react to its inputs changing instead. ## How to diagnose it 1. Put a log statement or breakpoint in the child component's constructor or `ngOnInit`; if it fires on unrelated updates, the view is being rebuilt. 2. Check which outlet input is bound to an expression that returns a new reference each time. 3. Hoist that value to a stable field or a `computed`, and keep per-render variation inside the context object. ## A quick reference for reviewers When reviewing a template that uses `NgTemplateOutlet`, look at each of its three bindings: - **`ngTemplateOutlet`**: should be a template reference variable, a query result or a `computed` that returns one of a fixed set of `TemplateRef`s. - **`ngTemplateOutletContext`**: may be an inline object literal; this is the input designed to vary. - **`ngTemplateOutletInjector`**: should be a field created once, the string `'outlet'`, or left unset. ## The code-level equivalent If you render templates yourself with `ViewContainerRef.createEmbeddedView()`, you own these decisions: clearing the container destroys views, and replacing the context object of an existing `EmbeddedViewRef` is no longer supported. That is why hand-written code usually mutates the existing context or keeps its own proxy, exactly as `NgTemplateOutlet` does.
- Does a child component inside the outlet re-run ngOnInit when the row in the context changes?No. A context change keeps the embedded view, so the child component instance survives and ngOnInit does not run again. The child must react to its inputs changing, for example through a signal input or ngOnChanges.
- What happens if ngTemplateOutlet is bound to null?The directive removes the current embedded view, which destroys it, and renders nothing until a template is bound again. A later non-null template creates a brand-new view.
Swapping the context is like changing the slide in a projector that stays switched on; swapping the template or injector is like unplugging the projector and setting up a new one, so everything on screen starts from scratch.
saying these in an interview costs you the question
- Thinks every context change destroys and recreates the view
- Creates an Injector in a template method call bound to the outlet
- Expects ngOnInit to re-run when the context row changes
- Believes switching between two TemplateRefs preserves child state
- Avoids object literals in ngTemplateOutletContext fearing view rebuilds