Angular Best Practices
Documentation for Developers
Angular 20 Development Best Practices
This guide outlines our Angular 20 development standards for building modern, reactive, and maintainable applications using standalone components and signals.
File Structure and Component Organization
1. Separate Files: Always separate Angular components into distinct files:
.tsfor component logic.htmlfor templates.scssfor styles
2. Component Architecture: Segregate functionality using parent/child components for better debugging and maintainability. Follow Angular's latest best practices for parent-child data connection using signals for maximum reactivity.
Signal-Based Reactive Programming
1. Use Signals with Automatic Change Detection
Always favor Signal, Computed, and Effect over traditional @Input(), @Output(), RxJS, or ChangeDetectorRef unless interop is necessary.
Prefer InputSignal<T> over @Input():
title: InputSignal<string> = input('Default Title');
- Ensure all components and directives update UI reactively via signals
- No need for manual
markForCheckwhen signals are used properly - Apply
allowSignalWrites: truewhen writing to signals insideeffect()
2. Keep Templates Clean with Minimal Logic
Never include &&, ?:, or logic-heavy conditions directly inside templates.
Move all logic to computed() signals:
readonly shouldShowButton = computed(() => this.mode() === 'active');
@if (shouldShowButton()) {
<button>Click</button>
}
3. Avoid Complex Computations in Property Bindings
NEVER do math or deep object access in templates. Always prepare display values in signals:
formattedPrice = computed(() => `$${this.price().toFixed(2)}`);
<span [textContent]="formattedPrice()"></span>
Modern Angular Control Flow
4. Always Use Built-in Control Flow (@if/@for/@switch)
Use Angular's built-in control flow and avoid legacy structural directives:
@if (items.length > 0) {
@for (item of items; track item.id) {
<div>{{ item.name }}</div>
}
}
@switch (status()) {
@case ('loading') { <spinner></spinner> }
@case ('error') { <error-view></error-view> }
@default { <content-view></content-view> }
}
Do:
- Prefer
@if,@for, and@switch - In
@for, always set an explicit track strategy:track item.idor a tracking function - Always end HTML files with a newline character - Angular's template parser expects this
Do NOT:
- Use
*ngIf,*ngFor, or[ngSwitch] - Import
CommonModulejust to getNgIf,NgFor, orNgSwitch - End HTML template files without a trailing newline (causes parser errors)
Benefits: Improved performance, better scoping, clearer structure, and more compiler optimizations.
5. HTML Template File Requirements
Critical: Always ensure HTML template files end with a newline character.
<div class="my-component">
<h1>{{ title() }}</h1>
@if (showContent()) {
<p>Content here</p>
}
</div>
<!-- ↑ Must have newline after closing tag -->
Why this matters:
- Angular's template parser expects a newline at the end of files
- Missing newline can cause "Unexpected closing tag" errors
- This is a standard Unix/POSIX file format requirement
How to verify:
# Check if file ends with newline
tail -c 1 myfile.html | od -c
# Should show: \n
# Add newline if missing
echo "" >> myfile.html
5. Implement Structural Directives Efficiently
Create standalone structural directives using standalone: true.
✅ Prefer:
@if (isEnabled) {
@for (item of items; track item.id) {
<ng-container>...</ng-container>
}
}
❌ Avoid:
<div *ngIf="isEnabled"><div *ngFor="..."></div></div>
Template and UI Best Practices
6. Use ngTemplateOutlet for Reusable Template Parts
Extract complex inner views into ng-template and reuse using *ngTemplateOutlet:
<ng-container *ngTemplateOutlet="customTemplate; context: context"></ng-container>
7. Use @defer for Heavy/Lazy Content
Defer rendering of expensive blocks and components to improve TTI:
@defer (on idle) {
<heavy-chart [data]="chartData()"></heavy-chart>
} @placeholder {
<skeleton-chart></skeleton-chart>
}
Accessibility and Styling
8. Apply Accessibility Attributes Consistently
Always use aria-*, role, and keyboard interaction (e.g., tabindex) where appropriate.
Reuse accessibility attributes through host bindings or directives:
host: {
'role': 'button',
'aria-pressed': 'true',
'tabindex': '0'
}
9. Apply Consistent CSS Patterns
- Always use utility-first CSS like Tailwind or Angular-hosted CSS variables
- Prefer component-level SCSS encapsulation and avoid deep selectors
- When theming, use global tokens from a theme system (e.g.,
--primary,--bg-muted,--text-color)
Development Guidelines Summary
When generating Angular code or making suggestions:
- Always use Angular 20, Standalone APIs, and Signals
- Promote clean templates, accessibility, and performance-first patterns
- Default to @if/@for/@switch, computed signals, and directive reuse
- Ensure styles are modular and scoped, using our theme-system library, TailwindCSS or CSS Variables
Architecture Principles
Dependency Injection
- Use the
injectfunction for dependency injection in services and components - Prefer constructor injection for services that need initialization
- Use
providedIn: 'root'for singleton services
Modern Initialization Patterns (Angular 19+)
App Initialization with provideAppInitializer
Use provideAppInitializer instead of the deprecated APP_INITIALIZER token for application bootstrap initialization:
import { provideAppInitializer, inject } from '@angular/core';
import { ConfigService } from './services/config.service';
export function initializeApp() {
return () => {
const configService = inject(ConfigService);
return configService.loadConfig(); // Returns Promise<void>
};
}
// In main.ts or app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideAppInitializer(initializeApp)
]
};
Environment Initialization with provideEnvironmentInitializer
Use provideEnvironmentInitializer instead of the deprecated ENVIRONMENT_INITIALIZER token for environment setup:
import { provideEnvironmentInitializer, inject, isDevMode } from '@angular/core';
// In provider function
return makeEnvironmentProviders([
provideEnvironmentInitializer(() => {
if (isDevMode()) {
console.log('🚀 Application environment initialized');
// Setup dev tools, logging, etc.
}
})
]);
Key Differences:
provideAppInitializer: For async operations that must complete before app bootstrap (returns Promise)provideEnvironmentInitializer: For sync setup tasks during environment creation (returns void)- Modern approach: Uses functions instead of injection tokens for better type safety and DX
State Management
- Use signals for local component state
- Use NGXS for complex global state management
- Prefer computed signals for derived state
- Use effects for side effects and reactive operations
Performance
- Implement OnPush change detection strategy where appropriate
- Use
@deferfor lazy loading heavy components - Minimize template logic and move computations to signals
- Use track functions in
@forloops for optimal rendering
Code Quality
- Use strict TypeScript typing - avoid
any - Implement proper error handling with catchError
- Use consistent naming conventions
- Follow SOLID principles and DRY patterns
- Write clean, robust, and maintainable code
Import Organization
Follow proper import order:
- Angular core/common imports
- RxJS imports
- Core library imports (@widgets-editor/core)
- Shared library imports (@widgets-editor/shared-ui)
- Feature imports
- Relative path imports
Styling
- Use 2-space indentation and single quotes for strings
- Write SCSS code using inheritance and nesting
- Place child selectors inside parent selectors:
.class-father {
.class-child {
.class-grand-child {
// styles
}
}
}
Avoid this:
effect(() => {
allowSignalWrites: true,
...
});
Use this instead:
effect(() => {
...
});
Why?
The 'allowSignalWrites' flag is deprecated and no longer impacts effect() (writes are always allowed).
Prohibited Practices
- Never use setTimeout() - use reactive patterns instead (signals, effects, computed)
- Never use modules - always use standalone components
- Never inline templates or styles - always use separate files
- Never use legacy control flow - use
@if/@for/@switch - Never bypass change detection - use signals for reactivity
❌ Never Use setTimeout() - Use Reactive Patterns Instead
Why setTimeout() is prohibited:
- Creates timing dependencies and race conditions
- Bypasses Angular's change detection system
- Makes code harder to test and debug
- Indicates underlying architectural issues that should be fixed properly
❌ DON'T:
// BAD: Using setTimeout() for change detection
ngOnInit() {
setTimeout(() => {
this.cdr.markForCheck();
}, 100);
}
// BAD: Using setTimeout() for state updates
updateState() {
setTimeout(() => {
this.state = newValue;
}, 0);
}
✅ DO: Use Reactive Patterns:
// GOOD: Use signals for reactive state
state = signal(initialValue);
// GOOD: Use computed for derived state
derivedState = computed(() => this.state() * 2);
// GOOD: Use effects for side effects
effect(() => {
// Reactive side effects here
console.log('State changed:', this.state());
});
// GOOD: Use proper Angular lifecycle hooks
ngOnInit() {
// Proper initialization without setTimeout
this.initializeComponent();
}
When you think you need setTimeout():
- State Updates: Use signals and computed values
- Change Detection: Fix the root cause, don't force it
- Async Operations: Use proper async/await patterns
- UI Updates: Use Angular's reactive system with signals
- Timing Issues: Use Angular's built-in timing mechanisms
Deprecated Patterns to Avoid (Angular 19+)
❌ Deprecated Initialization Tokens
// DON'T: Use deprecated tokens
{ provide: APP_INITIALIZER, useFactory: () => initFunction, multi: true }
{ provide: ENVIRONMENT_INITIALIZER, useValue: () => setupFunction, multi: true }
// DO: Use modern Angular 19+ functions
provideAppInitializer(() => initFunction)
provideEnvironmentInitializer(() => setupFunction)
✅ Modern Initialization Functions
// DO: Use modern provider functions
provideAppInitializer(() => initFunction())
provideEnvironmentInitializer(() => setupFunction())
❌ Deprecated Signal Effect Options
// DON'T: Use deprecated allowSignalWrites
effect(() => {
// logic here
}, { allowSignalWrites: true }); // This flag is deprecated
✅ Modern Effect Usage
// DO: Signal writes are always allowed in effects now
effect(() => {
// Signal writes work automatically
mySignal.set(newValue);
});
Migration Benefits:
- Better Type Safety: Functions provide better TypeScript integration
- Cleaner Code: More readable and maintainable initialization patterns
- Future-Proof: Aligned with Angular's modern architecture direction
- Performance: Optimized initialization pipeline
This documentation ensures consistent, modern Angular development practices across our monorepo.