18 KiB
18 KiB
| 1 | No | Category | Guideline | Description | Do | Don't | Code Good | Code Bad | Severity | Docs URL |
|---|---|---|---|---|---|---|---|---|---|---|
| 2 | 1 | Components | Use standalone components | Angular 17+ default; no NgModule needed | Standalone components for all new code | NgModule-based components for new projects | @Component({ standalone: true imports: [CommonModule] }) | @NgModule({ declarations: [MyComp] }) | High | https://angular.dev/guide/components/importing |
| 3 | 2 | Components | Use signals for state | Signals are Angular's reactive primitive for fine-grained reactivity | Signals for component state over class properties | Mutable class properties without signals | count = signal(0); increment() { this.count.update(v => v + 1) } | count = 0; increment() { this.count++ } | High | https://angular.dev/guide/signals |
| 4 | 3 | Components | Use @if/@for/@switch control flow | Built-in control flow syntax replaces *ngIf/*ngFor directives | @if and @for in templates | *ngIf and *ngFor structural directives | @if (isLoggedIn) { <Dashboard /> } @else { <Login /> } | <div *ngIf="isLoggedIn"><Dashboard /></div> | High | https://angular.dev/guide/templates/control-flow |
| 5 | 4 | Components | Use input() and output() signals | Signal-based inputs/outputs replace @Input()/@Output() decorators | input() and output() for component API | @Input() and @Output() decorators | name = input<string>(); clicked = output<void>() | @Input() name: string; @Output() clicked = new EventEmitter() | High | https://angular.dev/guide/components/inputs |
| 6 | 5 | Components | Use content projection | ng-content for flexible component composition | ng-content with select for named slots | Rigid templates that can't be customized | <ng-content select="[header]" /> <ng-content /> | <div class="header">{{ title }}</div> | Medium | https://angular.dev/guide/components/content-projection |
| 7 | 6 | Components | Keep components small | Single responsibility; components should do one thing | Extract sub-components when template exceeds 50 lines | Monolithic components handling multiple concerns | <UserAvatar /> <UserDetails /> <UserActions /> | One 300-line component template | Medium | https://angular.dev/guide/components |
| 8 | 7 | Components | Use OnPush change detection | Reduces re-renders by only checking on input changes or signal updates | OnPush for all components | Default change detection strategy | changeDetection: ChangeDetectionStrategy.OnPush | changeDetection: ChangeDetectionStrategy.Default | High | https://angular.dev/guide/components/lifecycle |
| 9 | 8 | Components | Avoid direct DOM manipulation | Use renderer or ElementRef sparingly; prefer template bindings | Template bindings and Angular directives | Direct document.querySelector or innerHTML | [class.active]="isActive" | this.el.nativeElement.classList.add('active') | High | https://angular.dev/guide/components/host-elements |
| 10 | 9 | Routing | Lazy load feature routes | Load route chunks on demand to reduce initial bundle | loadComponent() for all feature routes | Eager-loaded routes in app config | { path: 'admin' loadComponent: () => import('./admin/admin.component') } | { path: 'admin' component: AdminComponent } | High | https://angular.dev/guide/routing/lazy-loading |
| 11 | 10 | Routing | Use route guards with functional API | Protect routes with canActivate/canMatch functional guards | Functional guards returning boolean or UrlTree | Class-based guards with CanActivate interface | canActivate: [() => inject(AuthService).isLoggedIn()] | canActivate: [AuthGuard] | High | https://angular.dev/guide/routing/common-router-tasks#preventing-unauthorized-access |
| 12 | 11 | Routing | Use route resolvers for data | Pre-fetch data before route activation using resolve | ResolveFn for route data | Fetching data in ngOnInit causing flash of empty state | resolve: { user: () => inject(UserService).getUser() } | Fetch in ngOnInit with loading state flickering | Medium | https://angular.dev/guide/routing/common-router-tasks#resolve |
| 13 | 12 | Routing | Type route params with inject | Use inject(ActivatedRoute) with signals or toSignal | Typed route params via ActivatedRoute | Untyped route.snapshot.params string access | const id = toSignal(route.paramMap.pipe(map(p => p.get('id')))) | const id = this.route.snapshot.params['id'] | Medium | https://angular.dev/api/router/ActivatedRoute |
| 14 | 13 | Routing | Use nested routes for layouts | Compose shared layouts using router-outlet nesting | Nested routes with shared layout components | Duplicating layout code across routes | { path: 'app' component: ShellComponent children: [...] } | Duplicate header/sidebar in each route component | Medium | https://angular.dev/guide/routing/router-tutorial-toh#child-route-configuration |
| 15 | 14 | Routing | Configure preloading strategies | Preload lazy modules in background after initial load | PreloadAllModules or custom strategy | No preloading causing delayed navigation | provideRouter(routes withPreloading(PreloadAllModules)) | provideRouter(routes) | Low | https://angular.dev/api/router/PreloadAllModules |
| 16 | 15 | State | Use signals for local state | Signals provide synchronous reactive state without RxJS overhead | signal() for component-local reactive state | BehaviorSubject for simple local state | const items = signal<Item[]>([]); addItem(i: Item) { this.items.update(arr => [...arr i]) } | items$ = new BehaviorSubject<Item[]>([]) | High | https://angular.dev/guide/signals |
| 17 | 16 | State | Use computed() for derived state | Lazily evaluated derived values that update when dependencies change | computed() for values derived from other signals | Duplicated state or manual sync | readonly total = computed(() => this.items().reduce((s i) => s + i.price 0)) | this.total = this.items.reduce(...) // called manually | High | https://angular.dev/guide/signals#computed-signals |
| 18 | 17 | State | Use effect() carefully | Effects run side effects when signals change; avoid overuse | effect() for side effects like logging or localStorage sync | effect() for deriving state (use computed instead) | effect(() => localStorage.setItem('cart' JSON.stringify(this.cart()))) | effect(() => { this.total.set(this.items().length) }) | Medium | https://angular.dev/guide/signals#effects |
| 19 | 18 | State | Use NgRx Signal Store for complex state | NgRx Signal Store is the modern lightweight state management for Angular | @ngrx/signals SignalStore for feature state | Full NgRx reducer/action/effect boilerplate for simple state | const Store = signalStore(withState({ count: 0 }) withMethods(s => ({ increment: () => patchState(s { count: s.count() + 1 }) }))) | createReducer(on(increment state => ({ ...state count: state.count + 1 }))) | Medium | https://ngrx.io/guide/signals |
| 20 | 19 | State | Inject services for shared state | Services with signals share state across components without a store | Injectable service with signals for cross-component state | Prop drilling or @Input chains for shared state | @Injectable({ providedIn: 'root' }) class CartService { items = signal<Item[]>([]) } | @Input() cartItems passed through 4 component levels | Medium | https://angular.dev/guide/di/creating-injectable-service |
| 21 | 20 | State | Avoid mixing RxJS and signals unnecessarily | Use toSignal() to bridge RxJS into signal world at the boundary | toSignal() to convert observable to signal at component edge | Subscribing in components and storing in signal manually | readonly user = toSignal(this.userService.user$) | this.userService.user$.subscribe(u => this.user.set(u)) | Medium | https://angular.dev/guide/rxjs-interop |
| 22 | 21 | Forms | Use typed reactive forms | FormGroup/FormControl with explicit generics for compile-time safety | FormBuilder with typed controls | Untyped FormControl or any casts | fb.group<LoginForm>({ email: fb.control('') password: fb.control('') }) | new FormGroup({ email: new FormControl(null) }) | High | https://angular.dev/guide/forms/typed-forms |
| 23 | 22 | Forms | Use reactive forms over template-driven | Reactive forms scale better and are fully testable | ReactiveFormsModule for all non-trivial forms | FormsModule with ngModel for complex forms | <input [formControl]="emailControl" /> | <input [(ngModel)]="email" /> | Medium | https://angular.dev/guide/forms/reactive-forms |
| 24 | 23 | Forms | Write custom validators as functions | Functional validators are composable and tree-shakeable | ValidatorFn functions for custom validation | Class-based validators implementing Validator interface | const noSpaces: ValidatorFn = ctrl => ctrl.value?.includes(' ') ? { noSpaces: true } : null | class NoSpacesValidator implements Validator { validate(c) {} } | Medium | https://angular.dev/guide/forms/form-validation#custom-validators |
| 25 | 24 | Forms | Use updateOn for performance | Control when validation runs to avoid per-keystroke validation overhead | updateOn: 'blur' or 'submit' for expensive validators | Default updateOn: 'change' for async validators | fb.control('' { updateOn: 'blur' validators: [Validators.email] }) | fb.control('' [Validators.email]) // validates on every key | Low | https://angular.dev/api/forms/AbstractControl#updateOn |
| 26 | 25 | Forms | Use FormArray for dynamic fields | FormArray manages variable-length lists of controls | FormArray for add/remove field scenarios | Manually tracking index-based controls | get items(): FormArray { return this.form.get('items') as FormArray } | items: [FormControl] managed outside form | Medium | https://angular.dev/guide/forms/reactive-forms#using-the-formarray-class |
| 27 | 26 | Forms | Display validation errors clearly | Use form control touched and dirty states to show errors at the right time | Show errors after field is touched | Show all errors on page load | @if (email.invalid && email.touched) { <span>Invalid email</span> } | @if (email.invalid) { <span>Invalid email</span> } | Medium | https://angular.dev/guide/forms/form-validation |
| 28 | 27 | Performance | Apply OnPush to all components | OnPush + signals eliminates most unnecessary change detection cycles | OnPush change detection everywhere | Default strategy which checks entire tree on every event | changeDetection: ChangeDetectionStrategy.OnPush | changeDetection: ChangeDetectionStrategy.Default | High | https://angular.dev/best-practices/skipping-component-subtrees |
| 29 | 28 | Performance | Use trackBy in @for blocks | Stable identity for list items prevents full DOM re-creation on change | track item.id in @for | @for (item of items; track item.id) { <li>{{ item.name }}</li> } | @for (item of items; track $index) { <li>{{ item.name }}</li> } | High | https://angular.dev/guide/templates/control-flow#track-and-identity | |
| 30 | 29 | Performance | Use @defer for below-the-fold content | Defer blocks lazy-load components when they enter the viewport | @defer with on viewport for non-critical UI | Eagerly loading all components at startup | @defer (on viewport) { <HeavyChart /> } @placeholder { <Skeleton /> } | <HeavyChart /> loaded at startup | High | https://angular.dev/guide/defer |
| 31 | 30 | Performance | Use NgOptimizedImage | Enforces image best practices: lazy loading LCP hints and proper sizing | NgOptimizedImage for all img tags | Plain img tags for CMS or user content | <img ngSrc="/hero.jpg" width="800" height="400" priority /> | <img src="/hero.jpg" /> | High | https://angular.dev/guide/image-optimization |
| 32 | 31 | Performance | Tree-shake unused Angular features | Import only what you use from Angular packages | Import specific Angular modules needed | Import BrowserAnimationsModule when not using animations | import { NgOptimizedImage } from '@angular/common' | import { CommonModule } from '@angular/common' // entire module | Medium | https://angular.dev/tools/cli/build |
| 33 | 32 | Performance | Avoid subscribe in components | Subscriptions leak and cause bugs; prefer async pipe or toSignal | toSignal() or async pipe instead of manual subscribe | Manual subscribe without unsubscribe in ngOnDestroy | readonly data = toSignal(this.service.data$) | this.service.data$.subscribe(d => this.data = d) | High | https://angular.dev/guide/rxjs-interop |
| 34 | 33 | Performance | Use SSR with Angular Universal | Pre-render pages for faster LCP and better SEO | SSR or SSG for public-facing routes | Pure CSR for SEO-critical pages | ng add @angular/ssr | // no SSR, client renders empty shell | Medium | https://angular.dev/guide/ssr |
| 35 | 34 | Performance | Minimize bundle with standalone APIs | Standalone components + provideRouter() eliminate dead NgModule code | provideRouter() and provideHttpClient() in app.config | Root AppModule with all imports | provideRouter(routes) in app.config.ts | @NgModule({ imports: [RouterModule.forRoot(routes)] }) | Medium | https://angular.dev/guide/routing/standalone |
| 36 | 35 | Testing | Use TestBed for component tests | TestBed sets up Angular DI for realistic component testing | TestBed.configureTestingModule for component tests | Instantiate components with new keyword | TestBed.configureTestingModule({ imports: [MyComponent] }) | const comp = new MyComponent() | High | https://angular.dev/guide/testing/components-basics |
| 37 | 36 | Testing | Use Angular CDK component harnesses | Harnesses provide a stable testing API that survives template refactors | MatButtonHarness and custom HarnessLoader | Direct native element queries that break on template changes | const btn = await loader.getHarness(MatButtonHarness) | fixture.debugElement.query(By.css('button')) | Medium | https://material.angular.io/cdk/test-harnesses/overview |
| 38 | 37 | Testing | Use Spectator for less boilerplate | Spectator wraps TestBed with a cleaner API reducing test setup noise | Spectator for unit tests | Raw TestBed for every test | const spectator = createComponentFactory(MyComponent) | TestBed.configureTestingModule({ declarations: [MyComponent] providers: [...] }) | Low | https://github.com/ngneat/spectator |
| 39 | 38 | Testing | Mock services with jasmine.createSpyObj | Isolate unit tests by providing mock implementations of dependencies | SpyObj or jest.fn() mocks for services | Real HTTP calls in unit tests | const spy = jasmine.createSpyObj('UserService' ['getUser']); spy.getUser.and.returnValue(of(user)) | providers: [UserService] // real service in unit test | High | https://angular.dev/guide/testing/services |
| 40 | 39 | Testing | Write integration tests for routes | Test full route navigation including guards and resolvers | RouterTestingHarness for route integration tests | Mock all routing behavior in unit tests | const harness = await RouterTestingHarness.create(); await harness.navigateByUrl('/home') | // manually calling route guard methods | Medium | https://angular.dev/api/router/testing/RouterTestingHarness |
| 41 | 40 | Testing | Test signal-based components | Signals update synchronously; no async flush needed in most cases | Read signal value directly in test assertions | TestBed.tick() or fakeAsync for signal reads | component.count.set(5); expect(component.double()).toBe(10) | fakeAsync(() => { component.count.set(5); tick(); expect(component.double()).toBe(10) }) | Medium | https://angular.dev/guide/testing |
| 42 | 41 | Styling | Use ViewEncapsulation.Emulated | Default emulation scopes styles to component preventing global leaks | Emulated or None for intentional global styles | ViewEncapsulation.None for component-specific styles | ViewEncapsulation.Emulated (default) | ViewEncapsulation.None on feature components | Medium | https://angular.dev/guide/components/styling#style-scoping |
| 43 | 42 | Styling | Use :host selector | Style the component's host element using :host pseudo-class | :host for host element styles | Adding wrapper div just for styling | :host { display: block; padding: 1rem } | <div class="wrapper">...</div> + .wrapper { padding: 1rem } | Medium | https://angular.dev/guide/components/styling#host-element |
| 44 | 43 | Styling | Use CSS custom properties for theming | CSS variables work across component boundaries and enable dynamic theming | CSS custom properties for colors and spacing | Hardcoded hex values in component styles | :root { --primary: #6200ee } button { background: var(--primary) } | button { background: #6200ee } | Medium | https://angular.dev/guide/components/styling |
| 45 | 44 | Styling | Integrate Tailwind with Angular | Tailwind utilities work alongside Angular's ViewEncapsulation via global stylesheet | Add Tailwind in styles.css and use utility classes in templates | Custom CSS for layout that Tailwind already handles | <div class="flex items-center gap-4 p-6"> | <div class="my-custom-flex"> /* .my-custom-flex { display: flex } */ | Low | https://tailwindcss.com/docs/guides/angular |
| 46 | 45 | Styling | Use Angular Material theming tokens | Material 3 uses design tokens for systematic theming | M3 token-based theming for Angular Material | Overriding Angular Material CSS with deep selectors | @include mat.button-theme($my-theme) | ::ng-deep .mat-button { background: red } | Medium | https://material.angular.io/guide/theming |
| 47 | 46 | Architecture | Use injection tokens for config | Provide configuration via InjectionToken for testability and flexibility | InjectionToken for environment-specific values | Importing environment.ts directly in services | const API_URL = new InjectionToken<string>('apiUrl'); provide: [{ provide: API_URL useValue: env.apiUrl }] | constructor(private env: Environment) { this.url = env.apiUrl } | Medium | https://angular.dev/guide/di/dependency-injection-providers#using-an-injectiontoken-object |
| 48 | 47 | Architecture | Use HTTP interceptors | Intercept requests for auth headers error handling and logging | Functional interceptors with withInterceptors() | Service-level header management in every request | withInterceptors([authInterceptor errorInterceptor]) | httpClient.get(url { headers: { Authorization: token } }) in every call | High | https://angular.dev/guide/http/interceptors |
| 49 | 48 | Architecture | Organize by feature not type | Feature-based folder structure scales better than type-based | Feature folders with collocated component service and routes | Flat folders: all-components/ all-services/ | src/features/checkout/checkout.component.ts checkout.service.ts checkout.routes.ts | src/components/checkout.component.ts src/services/checkout.service.ts | Medium | https://angular.dev/style-guide#folders-by-feature-structure |
| 50 | 49 | Architecture | Use environment configurations | Separate environment values for dev staging and prod via Angular build configs | angular.json fileReplacements for env configs | Hardcoded API URLs or feature flags in source | fileReplacements: [{ replace: environment.ts with: environment.prod.ts }] | const API = 'https://api.example.com' // hardcoded in service | High | https://angular.dev/tools/cli/environments |
| 51 | 50 | Architecture | Prefer inject() over constructor DI | inject() function is composable and works in more contexts than constructor injection | inject() for dependency injection | Constructor parameters for new code | readonly http = inject(HttpClient); readonly router = inject(Router) | constructor(private http: HttpClient private router: Router) {} | Medium | https://angular.dev/api/core/inject |