Angular 20 provides zoneless change detection as a stable feature, which this application uses for improved performance.
Note: Zoneless change detection is now stable in Angular 20. The experimental API
provideExperimentalZonelessChangeDetection()has been replaced withprovideZonelessChangeDetection().
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideZonelessChangeDetection(),
provideRouter(routes),
provideClientHydration(withEventReplay()),
provideHttpClient(withFetch()),
provideAnimationsAsync(),
// ... other providers
],
};- No Zone.js dependency: Reduces bundle size and improves startup performance
- Better performance: Manual change detection triggering when needed
- Signal-first: Encourages use of Angular Signals for reactive programming
- Future-ready: Prepares codebase for Angular's direction
- Use Angular Signals for state management instead of RxJS for simple data
- Manual change detection may be needed for some third-party integrations
- Testing adjustments may be required for components that rely on automatic change detection
- Event handling works as expected with Angular's event system
Angular Signals are the preferred way to manage state in this application.
@Component({
selector: 'lfx-example',
imports: [CommonModule, ReactiveFormsModule],
template: `
@if (loading()) {
<div>Loading...</div>
} @else if (error()) {
<div>Error: {{ error() }}</div>
} @else {
@for (item of data(); track item.id) {
<div>{{ item.name }}</div>
}
}
`,
})
export class ExampleComponent {
private readonly service = inject(DataService);
// Access reactive signals from service
public readonly data = this.service.data;
public readonly loading = this.service.loading;
public readonly error = this.service.error;
}@Injectable({ providedIn: 'root' })
export class DataService {
// Private state signals
private readonly dataSignal = signal<Data[]>([]);
private readonly loadingSignal = signal<boolean>(false);
private readonly errorSignal = signal<string | null>(null);
// Public readonly signals
public readonly data = this.dataSignal.asReadonly();
public readonly loading = this.loadingSignal.asReadonly();
public readonly error = this.errorSignal.asReadonly();
// Computed signals for derived state
public readonly activeData = computed(() => this.dataSignal().filter((item) => item.is_active));
}// app.config.server.ts
export const config = mergeApplicationConfig(appConfig, {
providers: [provideServerRendering(withRoutes(serverRoutes))],
});// app.routes.server.ts
export const serverRoutes: ServerRoute[] = [
{
path: '**',
renderMode: RenderMode.Server,
},
];- Improved SEO: Pre-rendered content for search engines
- Faster initial load: Content visible before JavaScript loads
- Better UX: Reduced time to first contentful paint
- Progressive enhancement: Works without JavaScript enabled
All components use Angular's standalone component pattern:
@Component({
selector: 'lfx-example',
imports: [CommonModule, ReactiveFormsModule, ButtonModule],
template: `<!-- template here -->`,
})
export class ExampleComponent {
// Component implementation
}- Explicit dependencies: Clear import requirements
- Tree-shakable: Only imported code is bundled
- Modular: Components are self-contained
- Future-ready: Angular's recommended approach
Modern Angular template syntax used throughout:
<!-- Control flow -->
@if (condition) {
<div>Content</div>
} @else {
<div>Alternative</div>
}
<!-- For loops with track -->
@for (item of items(); track item.id) {
<div>{{ item.name }}</div>
}
<!-- Signal interpolation -->
{{ signalValue() }}For data transformation in templates, always use Angular pipes instead of component methods:
// ❌ BAD: Using methods in templates
@Component({
template: `<span>{{ formatDate(item.created_at) }}</span>`,
})
export class BadComponent {
formatDate(date: string): string {
return new Date(date).toLocaleDateString();
}
}
// ✅ GOOD: Using pipes in templates
@Component({
template: `<span>{{ item.created_at | date: 'MMM d, y' }}</span>`,
})
export class GoodComponent {
// No method needed - use Angular's built-in date pipe
}- Performance: Pipes are pure by default and cached - they only re-execute when inputs change
- Reusability: Pipes can be shared across components
- Testability: Easier to unit test pipes in isolation
- Separation of Concerns: Keeps components focused on logic, not formatting
- Change Detection: Methods are called on every change detection cycle, pipes only when needed
<!-- Date formatting -->
{{ dateValue | date: 'MMM d, y' }} {{ dateValue | date: 'MMM d, yyyy @ h:mm a' }}
<!-- Number formatting -->
{{ numberValue | number: '1.2-2' }} {{ price | currency: 'USD' }}
<!-- Text transformation -->
{{ text | uppercase }} {{ text | lowercase }} {{ text | titlecase }}
<!-- Array/Object manipulation -->
{{ items | slice: 0:10 }} {{ object | json }}export class ExampleComponent {
// Required inputs
public readonly title = input.required<string>();
public readonly description = input.required<string>();
// Optional inputs with defaults
public readonly isActive = input<boolean>(false);
public readonly size = input<'small' | 'large'>('small');
}export class ExampleComponent {
// Output events
public readonly onClick = output<Event>();
public readonly valueChange = output<string>();
// Event handlers
public handleClick(event: Event): void {
this.onClick.emit(event);
}
}With zoneless change detection:
- Signals automatically trigger updates when values change
- Event handlers trigger change detection
- Async operations may need manual triggering
- Third-party libraries may require integration adjustments
- Use signals for all reactive state
- Avoid RxJS for simple data flows
- Leverage computed signals for derived data
- Use track functions in
@forloops - Minimize DOM manipulations outside Angular