web-framework-angular-standalone · git:20260906.ae0cc61 · 2026-09-06 · sha256 803f3dc63faee6d7
web-framework-angular-standalone git:20260906.ae0cc61A
Immutable. This exact content is served forever at /api/v1/blob/803f3dc63faee6d7.
---
name: web-framework-angular-standalone
description: Angular 17–19 standalone components, signals, built-in control flow, inject() DI, the resource API. Load when writing or migrating Angular components.
---
# Angular Standalone Patterns
> **Quick Guide:** Components are standalone and declare their own `imports`; NgModules are opt-in. State is signals — `signal()`, `computed()`, `linkedSignal()` for derived state you also write to, `effect()` only for genuine side effects and `afterRenderEffect()` for DOM work. Communication is `input()`, `output()` and `model()`. Templates use `@if`, `@for` (always with `track`), `@switch` and `@defer`, none of which need an import. Dependencies come from `inject()`.
**Detailed Resources:**
- [examples/core.md](examples/core.md) — a complete standalone component, signals, control flow, parent-child communication
- [examples/dependency-injection.md](examples/dependency-injection.md) — `inject()`, `InjectionToken`, injection options
- [examples/model.md](examples/model.md) — `model()` two-way binding, and when `input()` + `output()` is the better fit
- [examples/defer.md](examples/defer.md) — every `@defer` trigger, with prefetch and placeholder timing
- [examples/angular-19-features.md](examples/angular-19-features.md) — `linkedSignal()`, `resource()`, `rxResource()`, `afterRenderEffect()` phases
- [examples/rxjs.md](examples/rxjs.md) — `toSignal()`, `toObservable()`, and which of the two a problem wants
- [reference.md](reference.md) — decision trees, anti-patterns with corrected code, API and syntax tables, component checklist
---
## Which path applies
- **Angular 19** — `standalone: true` is the default, so the flag is noise; write `standalone: false` only for a component that genuinely belongs to an NgModule.
- **Angular 17–18** — the same patterns apply, but `standalone: true` is stated explicitly on every component, directive and pipe.
- **Signal APIs by version** — `linkedSignal()` and `afterRenderEffect()` land in 19, `httpResource()` in 19.2, and the resource family is still experimental. [examples/angular-19-features.md](examples/angular-19-features.md) marks each one.
---
<critical_requirements>
## Before writing Angular code
**Declare inputs and outputs with `input()`, `output()` and `model()`.** They are signals, so a `computed()` can depend on an input directly and no `ngOnChanges` is needed to notice it changed.
**Take dependencies with `inject()` in a field initialiser.** It works outside a constructor — in a function, a route guard, a factory — where constructor parameters cannot reach.
**Write templates with `@if`, `@for`, `@switch` and `@defer`.** They need no import, narrow types better than the structural directives, and `@for` has `@empty` and the `$index`/`$first`/`$last` context built in.
**Give every `@for` a `track` expression.** Without a stable key Angular rebuilds the rows rather than moving them, which loses focus and element state along with the performance.
**Update a signal through `.set()` or `.update()` returning a new reference.** Equality is `Object.is`, so mutating the array or object in place leaves the reference unchanged and nothing is notified.
**Use `linkedSignal()` for derived state that is also writable.** It recomputes from its source and still accepts a direct write, which is what a pair of signals kept in step by an `effect()` was imitating.
</critical_requirements>
---
**Auto-detection:** Angular standalone component, signal, computed, effect, linkedSignal, resource, rxResource, httpResource, input(), output(), model(), @if, @for, @switch, @defer, inject(), InjectionToken, provideRouter, bootstrapApplication, afterRenderEffect, afterNextRender, DestroyRef, toSignal, toObservable, viewChild, viewChildren
**Applies to:**
- Standalone components, their `imports` array and their providers
- Signal state: `signal`, `computed`, `linkedSignal`, `effect`, `afterRenderEffect`
- Component communication with `input`, `output` and `model`
- Built-in control flow and `@defer` lazy loading
- Dependency injection with `inject()` and injection tokens
- Application bootstrap and standalone route configuration
- The resource API, and interop between signals and observables
**Handled elsewhere:**
- Styling — a component names its `styles` or `styleUrl` and settles nothing about what goes in them
- Application-wide state stores layered above component signals
- Server-state caching and invalidation policy
- Test doubles for the network
---
<philosophy>
## Philosophy
Standalone removed the second declaration site. A component names what it uses in its own `imports`, so the dependency graph is readable from the component and a lazy route can point at a component rather than at a module wrapping one.
Signals then removed the second question. Change detection used to ask "what might have changed?" and walk the tree; a signal records who read it, so an update notifies exactly those consumers. That is why the guidance keeps pushing work down the chain: `computed()` where a value is derived, `linkedSignal()` where it is derived and writable, `effect()` only where something outside the graph has to happen — and `afterRenderEffect()` where that something is the DOM, because it runs in phases that keep reads and writes from thrashing layout.
</philosophy>
---
<patterns>
## Core patterns
### Pattern 1: Standalone component
A component declares its own imports and communicates through signal functions.
```typescript
@Component({
selector: "app-user-card",
imports: [DatePipe],
template: `
<h2>{{ user().name }}</h2>
<time>{{ user().createdAt | date: "mediumDate" }}</time>
<button (click)="edit.emit(user())">Edit</button>
`,
})
export class UserCardComponent {
user = input.required<User>();
edit = output<User>();
}
```
Full code: [examples/core.md](examples/core.md)
---
### Pattern 2: Signals
```typescript
count = signal(0);
doubleCount = computed(() => this.count() * 2);
this.count.set(5);
this.count.update((value) => value + 1);
items = signal<Item[]>([]);
this.items.update((items) => [...items, newItem]);
```
`computed()` is memoised and lazy; a method with the same body recomputes on every template read. Reserve `effect()` for logging, analytics, storage and other work outside the signal graph.
Full code: [examples/core.md](examples/core.md)
---
### Pattern 3: linkedSignal for writable derived state
```typescript
options = input.required<Option[]>();
selected = linkedSignal(() => this.options()[0]);
```
`selected` follows `options` and still accepts `selected.set(...)` from a click. Its computation form takes the previous value, which is how a selection survives a source change instead of resetting.
Full code: [examples/angular-19-features.md](examples/angular-19-features.md)
---
### Pattern 4: Inputs, outputs and model
```typescript
placeholder = input("Search...");
minLength = input.required<number>();
query = model("");
search = output<string>();
isValidSearch = computed(() => this.query().length >= this.minLength());
```
`model()` gives the parent `[(query)]`. Reach for it where the child genuinely owns the edit; `input()` plus `output()` keeps the flow one-way and is the better default.
Full code: [examples/model.md](examples/model.md)
---
### Pattern 5: Control flow
```html
@switch (state()) { @case ("loading") {
<div>Loading…</div>
} @case ("error") { <button (click)="retry.emit()">Retry</button> } @case
("success") { @for (user of users(); track user.id; let i = $index) {
<li>{{ i + 1 }}. {{ user.name }}</li>
} @empty {
<li>No users found</li>
} } }
```
`@if (user(); as user)` binds the narrowed value, so the signal is called once rather than in every expression beneath it.
Full code: [examples/core.md](examples/core.md)
---
### Pattern 6: @defer
```html
@defer (on viewport) {
<app-heavy-chart />
} @placeholder (minimum 200ms) {
<div class="chart-skeleton"></div>
} @loading (after 100ms; minimum 500ms) {
<div class="spinner"></div>
} @error {
<div>Failed to load chart</div>
}
```
`@placeholder` reserves the space, and the `after`/`minimum` timings on `@loading` are what stop a fast load flashing a spinner. Defer what is below the fold, behind an interaction, or conditional — never what is visible on arrival, which trades bundle size for LCP.
Full code: [examples/defer.md](examples/defer.md)
---
### Pattern 7: Dependency injection
```typescript
@Injectable({ providedIn: "root" })
export class UserService {
private http = inject(HttpClient);
private config = inject(CONFIG_TOKEN, { optional: true });
}
```
`inject()` also takes `{ skipSelf: true }` to start at the parent injector and `{ self: true }` to refuse to leave the current one. It must run in an injection context — a field initialiser or a constructor — never inside a method.
Full code: [examples/dependency-injection.md](examples/dependency-injection.md)
---
### Pattern 8: Bootstrap and routes
```typescript
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(
routes,
withComponentInputBinding(),
withPreloading(PreloadAllModules),
),
provideHttpClient(),
],
};
bootstrapApplication(AppComponent, appConfig);
export const routes: Routes = [
{
path: "users/:id",
loadComponent: () =>
import("./users/user-detail.component").then(
(m) => m.UserDetailComponent,
),
},
];
```
`loadComponent` lazy-loads a component with no wrapper module. `withComponentInputBinding()` binds route and query params straight to `input()` signals, so a route component needs no `ActivatedRoute` — a query param that may be absent is typed `input<string | undefined>()`.
---
### Pattern 9: Async data with resource()
```typescript
userResource = resource({
params: () => ({ id: this.userId() }),
loader: async ({ params, abortSignal }) => {
const response = await fetch(`/api/users/${params.id}`, {
signal: abortSignal,
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return (await response.json()) as User;
},
});
```
The resource re-runs when `params` changes and aborts the superseded request, so the signal-plus-effect combination that used to race is not needed. Guard reads with `hasValue()`, which narrows the type as well as the state. `rxResource()` takes an observable loader and `httpResource()` (19.2) goes through `HttpClient` and its interceptors.
Full code: [examples/angular-19-features.md](examples/angular-19-features.md)
---
### Pattern 10: Lifecycle and DOM effects
```typescript
private destroyRef = inject(DestroyRef);
private elementRef = inject(ElementRef);
width = signal(0);
constructor() {
afterNextRender(() => {
const observer = new ResizeObserver(([entry]) => this.width.set(entry.contentRect.width));
observer.observe(this.elementRef.nativeElement);
this.destroyRef.onDestroy(() => observer.disconnect());
});
}
```
| Legacy hook | Signal-era replacement |
| -------------------- | -------------------------------------------- |
| `ngOnInit` | field initialiser, or `effect()` |
| `ngOnChanges` | `effect()` reading the `input()` signal |
| `ngAfterViewInit` | `afterNextRender()` |
| `ngAfterViewChecked` | `afterRender()` (`afterEveryRender()` in 20) |
| `ngOnDestroy` | `DestroyRef.onDestroy()` |
| DOM side effects | `afterRenderEffect()` with explicit phases |
Full code: [examples/angular-19-features.md](examples/angular-19-features.md)
---
### Pattern 11: Observable interop
```typescript
users = toSignal(this.userService.getUsers(), { initialValue: [] });
count$ = toObservable(this.count);
```
`toSignal()` needs an `initialValue` for any source that has not emitted yet; without one the signal's type includes `undefined` and a template read before the first emission throws.
Full code: [examples/rxjs.md](examples/rxjs.md)
</patterns>
---
<red_flags>
## Red flags
**Breaks at runtime:**
- A signal's value mutated in place — `items().push(x)` leaves the reference identical, so `Object.is` reports no change and nothing re-renders
- `inject()` called from a method — it needs an injection context, and throws outside one
- `toSignal()` without `initialValue` on a source that has not emitted — reads before the first emission fail
- `resource()`, `rxResource()` or `httpResource()` used for a write — all three are read-only; a POST, PUT or DELETE goes through `HttpClient`
- `resource.value()` read without checking `hasValue()` — the guard is what narrows away the loading and error states
- `@for` without `track` — Angular tears down and rebuilds each row, discarding focus, scroll position and animation state
- A cleanup function returned from `effect()` — the return value is ignored, so a timer or subscription opened there leaks on every re-run; teardown goes in the `onCleanup` callback the effect body is handed as its argument
**Surprising behaviour:**
- `standalone: true` is the default from 19, so writing it is harmless noise while writing nothing is correct — the two look identical in review
- `@defer` renders its `@placeholder` during server rendering and ignores every trigger there
- `linkedSignal()` resets to its computed value whenever the source changes; preserving a user's choice needs the computation form that receives the previous value
- `allowSignalWrites` was removed in 19 and writing a signal inside an `effect()` is now allowed — which makes an effect-maintained derived value compile quietly where it used to complain
- `afterRenderEffect()` defaults to the `mixedReadWrite` phase, the one that thrashes layout; name `earlyRead` and `write` instead
- `signal()` compares with `Object.is`, so two structurally equal objects count as a change unless a custom `equal` is supplied
- Effects run during change detection from 19, not as microtasks, so ordering assumptions from earlier versions no longer hold
</red_flags>