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