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:

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');

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:

Do NOT:

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:

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

Development Guidelines Summary

When generating Angular code or making suggestions:

Architecture Principles

Dependency Injection

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:

State Management

Performance

Code Quality

Import Organization

Follow proper import order:

  1. Angular core/common imports
  2. RxJS imports
  3. Core library imports (@widgets-editor/core)
  4. Shared library imports (@widgets-editor/shared-ui)
  5. Feature imports
  6. Relative path imports

Styling

.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

Why setTimeout() is prohibited:

❌ 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():

  1. State Updates: Use signals and computed values
  2. Change Detection: Fix the root cause, don't force it
  3. Async Operations: Use proper async/await patterns
  4. UI Updates: Use Angular's reactive system with signals
  5. 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:

This documentation ensures consistent, modern Angular development practices across our monorepo.