Angular 21 Best Practices
TypeScript
- Use strict type checking
- Prefer type inference when type is obvious
- Avoid
any; use unknown when type is uncertain
Components
- Always use standalone components (do NOT set
standalone: true — it's the default in v20+)
- Set
changeDetection: ChangeDetectionStrategy.OnPush
- Use
input() and output() functions instead of decorators
- Use
computed() for derived state
- Keep components small and single-responsibility
- Prefer inline templates for small components
- Use Reactive forms over Template-driven
- Use
class bindings instead of ngClass
- Use
style bindings instead of ngStyle
- For external templates/styles, use paths relative to the component TS file
- Do NOT use
@HostBinding/@HostListener — use the host object in the decorator instead
State Management with Signals
- Use signals for local component state
- Use
computed() for derived state
- Keep state transformations pure and predictable
- Do NOT use
mutate on signals — use update or set instead
For complex derived state patterns, see references/signal-patterns.md.
Resources (Async Data)
Use resource() for async data fetching with signals:
const userResource = resource({
params: () => ({ id: userId() }),
loader: ({ params, abortSignal }) => fetch(`/api/users/${params.id}`, { signal: abortSignal }),
});
const userName = computed(() => userResource.hasValue() ? userResource.value().name : undefined);
Key resource patterns:
params returns undefined → loader doesn't run, status becomes 'idle'
- Use
abortSignal to cancel in-flight requests
- Check
hasValue() before accessing value() to handle loading/error states
- Status values:
'idle', 'loading', 'reloading', 'resolved', 'error', 'local'
Templates
- Use native control flow:
@if, @for, @switch (NOT *ngIf, *ngFor, *ngSwitch)
- Use async pipe for observables
- Keep templates simple — no complex logic
- Do NOT use arrow functions in templates (not supported)
- Do NOT assume globals like
new Date() are available
Services
- Single responsibility per service
- Use
providedIn: 'root' for singletons
- Use
inject() function instead of constructor injection
@Injectable({ providedIn: 'root' })
export class UserService {
private readonly http = inject(HttpClient);
}
Routing
- Implement lazy loading for feature routes:
export const routes: Routes = [
{
path: 'admin',
loadComponent: () => import('./admin/admin.page').then(m => m.AdminPage),
},
];
File Naming
- Routable view components:
file-name.page.ts, file-name.page.html, file-name.page.css
- Regular components:
file-name.component.ts
- Services:
file-name.service.ts
Icons (ng-icon)
import { NgIcon, provideIcons } from '@ng-icons/core';
import { heroSparkles, heroTrash } from '@ng-icons/heroicons/outline';
@Component({
selector: 'app-example',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [NgIcon],
providers: [provideIcons({ heroSparkles, heroTrash })],
template: `<ng-icon name="heroSparkles" />`,
})
export class ExampleComponent {}
Images
- Use
NgOptimizedImage for all static images
NgOptimizedImage does NOT work for inline base64 images
Styling
- Use Tailwind 4.1 for CSS (see tailwind skill if available)
- Angular CDK is available when needed
Accessibility
- MUST pass all AXE checks
- MUST meet WCAG AA minimums: focus management, color contrast, ARIA attributes
1---2name: angular-best-practices3description: Angular 21 development with modern best practices including signals, standalone components, reactive patterns, and accessibility. Use when creating Angular components, services, templates, or performing any Angular frontend development work. Covers TypeScript strict typing, signal-based state management, reactive forms, lazy loading, ng-icon setup, and Tailwind styling.4---5
6# Angular 21 Best Practices
7
8## TypeScript
9
10- Use strict type checking
11- Prefer type inference when type is obvious
12- Avoid `any`; use `unknown` when type is uncertain
13
14## Components
15
16- Always use standalone components (do NOT set `standalone: true` — it's the default in v20+)
17- Set `changeDetection: ChangeDetectionStrategy.OnPush`
18- Use `input()` and `output()` functions instead of decorators
19- Use `computed()` for derived state
20- Keep components small and single-responsibility
21- Prefer inline templates for small components
22- Use Reactive forms over Template-driven
23- Use `class` bindings instead of `ngClass`
24- Use `style` bindings instead of `ngStyle`
25- For external templates/styles, use paths relative to the component TS file
26- Do NOT use `@HostBinding`/`@HostListener` — use the `host` object in the decorator instead
27
28## State Management with Signals
29
30- Use signals for local component state
31- Use `computed()` for derived state
32- Keep state transformations pure and predictable
33- Do NOT use `mutate` on signals — use `update` or `set` instead
34
35For complex derived state patterns, see [references/signal-patterns.md](references/signal-patterns.md).
36
37## Resources (Async Data)
38
39Use `resource()` for async data fetching with signals:
40
41```typescript
42const userResource = resource({
43 params: () => ({ id: userId() }),
44 loader: ({ params, abortSignal }) => fetch(`/api/users/${params.id}`, { signal: abortSignal }),
45});
46
47const userName = computed(() => userResource.hasValue() ? userResource.value().name : undefined);
48```
49
50Key `resource` patterns:
51- `params` returns `undefined` → loader doesn't run, status becomes `'idle'`
52- Use `abortSignal` to cancel in-flight requests
53- Check `hasValue()` before accessing `value()` to handle loading/error states
54- Status values: `'idle'`, `'loading'`, `'reloading'`, `'resolved'`, `'error'`, `'local'`
55
56## Templates
57
58- Use native control flow: `@if`, `@for`, `@switch` (NOT `*ngIf`, `*ngFor`, `*ngSwitch`)
59- Use async pipe for observables
60- Keep templates simple — no complex logic
61- Do NOT use arrow functions in templates (not supported)
62- Do NOT assume globals like `new Date()` are available
63
64## Services
65
66- Single responsibility per service
67- Use `providedIn: 'root'` for singletons
68- Use `inject()` function instead of constructor injection
69
70```typescript
71@Injectable({ providedIn: 'root' })
72export class UserService {
73 private readonly http = inject(HttpClient);
74}
75```
76
77## Routing
78
79- Implement lazy loading for feature routes:
80
81```typescript
82export const routes: Routes = [
83 {
84 path: 'admin',
85 loadComponent: () => import('./admin/admin.page').then(m => m.AdminPage),
86 },
87];
88```
89
90## File Naming
91
92- Routable view components: `file-name.page.ts`, `file-name.page.html`, `file-name.page.css`
93- Regular components: `file-name.component.ts`
94- Services: `file-name.service.ts`
95
96## Icons (ng-icon)
97
98```typescript
99import { NgIcon, provideIcons } from '@ng-icons/core';
100import { heroSparkles, heroTrash } from '@ng-icons/heroicons/outline';
101
102@Component({
103 selector: 'app-example',
104 changeDetection: ChangeDetectionStrategy.OnPush,
105 imports: [NgIcon],
106 providers: [provideIcons({ heroSparkles, heroTrash })],
107 template: `<ng-icon name="heroSparkles" />`,
108})
109export class ExampleComponent {}
110```
111
112## Images
113
114- Use `NgOptimizedImage` for all static images
115- `NgOptimizedImage` does NOT work for inline base64 images
116
117## Styling
118
119- Use Tailwind 4.1 for CSS (see tailwind skill if available)
120- Angular CDK is available when needed
121
122## Accessibility
123
124- MUST pass all AXE checks
125- MUST meet WCAG AA minimums: focus management, color contrast, ARIA attributes