Angular Best Practices

repository·master·Indexed 41 days ago

https://github.com/evoytenkoapps/angular-best-practices

A collection of code style rules and architectural patterns for Angular projects. It provides a high-impact set of best practices for beginners to ensure maintainable and scalable codebases, covering file organization, module structure, naming conventions, RxJS usage, and OnPush change detection.

Tokens
9.5K
Snippets
29
Records
44
Agent score
0%

What's inside Angular Best Practices

  1. Coding principles: Demeter, DRY, and Mutation rules

    master

    Adhere to these core software engineering principles:

    • Law of Demeter: Use only one dot notation when accessing objects or services (e.g., someObject.someProperty). Avoid deep chaining like a.b.c.d.
    • DRY (Don't Repeat Yourself): Do not duplicate text, expressions, or logic. Move repeated code into constants, Enums, Classes, or functions.
    • Mutation Rules:
      • Components: Avoid mutations inside components to prevent issues with OnPush change detection.
      • Nested Functions: Do not change references to object properties from within nested functions. Instead, initialize the object where it belongs and use return values to update it.
    • Control Flow: Prefer if (condition) return; (early return) over if (condition) { ... } else { ... }.
  2. Naming Conventions: Use Nouns for Entities

    master

    Do not name interfaces or entities starting with verbs. Use nouns to describe what the entity is rather than what it does.

    • Bad: CreatePoTipInfoModel (starts with a verb)
    • Good: ProductOwnerTipInfoCreateModel (ends with the action/context, but the entity is a Model/Noun)
  3. Detect @Input changes using Setters or Getters

    master

    To react to @Input changes, prefer using a setter or a getter over ngOnChanges:

    • Use a Setter: When you need to run logic immediately when an input value changes. This provides better type safety than ngOnChanges.
    • Use a Getter: When you need to derive a value based on an input change. This is cleaner than manually updating a property in ngOnChanges.

    Note: If you must use ngOnChanges (e.g., to avoid 'glitch' effects), ensure you wrap SimpleChanges in a Generic.

    // Using a setter
    @Input()
    set userName(name: string) {
      console.log('name inside input was changed', name);
      this.firstName = name;
    }
    public firstName: string = '';
    
    // Using a getter
    public get isProductEditable(): boolean {
      return this.product.status === this.recordStatus.Approved && (!!this.isAdmin || !!this.isOwner);
    }
  4. Avoid nested subscriptions using higher-order operators

    master

    Never call .subscribe() inside another .subscribe() block. This creates complex, hard-to-manage code. Instead, use RxJS higher-order operators like switchMap to flatten the streams into a single subscription.

    Example Transformation: Instead of subscribing to StreamA and then subscribing to StreamB inside the callback, use StreamA.pipe(switchMap(val => StreamB(val))).subscribe(...).

    // Instead of nested subscribe:
    this.isModuleEditor$.pipe(
      switchMap(access => this.draftsService.getDrafts(DraftStatuses.Created, this.user.userId)),
      takeUntil(this.destroy$)
    ).subscribe((result) => {
      if (result) {
        this.containersService.pendingDraftState.next(result ? result : []);
      }
    });
  5. How to manage state in Angular

    master

    The choice of state management depends on the complexity of your business models:

    • Small scale (< 10 Business Models): Use Angular Services.
    • Large scale: Use a dedicated state manager (e.g., NGXS).

    Tracking Object Status

    To track the status of an object within the store, wrap it in a generic DataWithStatus<T> interface using a StatusData enum.

    The Facade Pattern

    To follow the Dependency Inversion principle, always interact with the Store through an abstract class called a Facade (e.g., UserInfoFacade).

    1. Provide the facade at the module level.
    2. Inject the abstract class into the component.
    3. Ensure the component/service contains no direct imports from the state management library.
    4. Use abstract, verb-based names for facade methods (e.g., load, update, set) rather than repeating state manager terminology.
    export enum StatusData {
      PENDING = 'PENDING',
      LOADING = 'LOADING',
      LOADED = 'LOADED',
      ERROR = 'ERROR',
      CANCEL = 'CANCEL',
    }
    
    export interface DataWithStatus<T> {
      readonly data: T;
      readonly status: StatusData;
    }
    
    // Usage in a state model
    export interface OpportunityModelStateModel {
      functionsAndSoft: DataWithStatus<FileToSave | null>;
    }
  6. Use RxJS instead of Promises

    master

    Avoid using Promise in favor of RxJS for the following reasons:

    1. Cancellation: RxJS provides built-in support for unsubscription/cancellation, whereas async/await requires manual management of cancellation tokens.
    2. Complexity Management: RxJS makes it easy to organize timeout and retry logic.
    3. Declarative Data Fetching: You can start requests declaratively using the | async pipe in templates without manual lifecycle management.
    4. State Dependency: RxJS simplifies making requests dependent on state changes like filtering, pagination, or searching.
  7. Structure Angular modules and folders

    master

    Split project logic into modules named according to business rules without a leading underscore (e.g., user-info). Inside each module, group folders using the following structure:

    • _components: Contains _smart (orchestrating components) and _dumb (presentational components) subfolders.
    • _providers: For dependency providers.
    • _models: For value objects, interfaces, and classes.
    • _pipes: For Angular pipes.
    • _directives: For Angular directives.
    • _guards: For route guards.
    • _resolvers: For route resolvers.
    • _store: For state management (e.g., NGXS), containing actions, models (states and defaults), and states (effects and selectors).
    • _constants: For constant values.
    • _services: Contains api (REST services) and facade (facade pattern implementations).

    Note: Mocks should be stored inside production folders (e.g., within the api service folder).

    ├───user-info
    │   │   user-info.module.ts
    │   │
    │   ├───user-software
    │   ├───_components
    │   │   ├───_dumb
    │   │   │   └───user-info-details
    │   │   │           user-info-details.component.ts
    │   │   │
    │   │   └───_smart
    │   │       └───user-info
    │   │               user-info.component.ts
    │   │
    │   ├───_constants
    │   ├───_directives
    │   ├───_guards
    │   ├───_models
    │   ├───_pipes
    │   ├───_providers
    │   │       uesr-http.provider.ts
    │   │
    │   ├───_resolvers
    │   ├───_services
    │   │   ├───api
    │   │   │       user-http-mock.service.ts
    │   │   │       user-http-prod.service.ts
    │   │   │       user-http.service.ts
    │   │   │
    │   │   └───facade
    │   │           user-ngxs.facade.ts
    │   │           user.facade.ts
    │   │
    │   └───_store
    │       ├───actions
    │       ├───models
    │       └───states
  8. How Smart and Dumb components work together

    master

    The project follows the smart/dumb component pattern to separate business logic from UI presentation.

    Smart Components

    • Role: Handle business logic and orchestration.
    • Communication: Interact directly with services (e.g., Facades).
    • Routing: Responsible for routing logic.
    • Data: Can show data and manage nested components.
    • Constraints: Should not use @Input or @Output for its own primary data flow.

    Dumb (Presentational) Components

    • Role: Purely presentational; they show data.
    • Communication: Data is passed in via @Input and events are emitted via @Output to parents/children.
    • Constraints: Should not use any services directly.
    • Naming: Do not name @Output properties after business logic (e.g., use deleteBtnChange instead of removeComponent).

    Data Transfer Pattern

    For complex parent-to-child communication, use an abstract class to define an interface, implement it in the child, and access the child via @ViewChild in the parent.

  9. Define data entities using DTO, Model, and VM

    master

    To maintain clean boundaries and follow the Dependency Inversion principle, categorize data into three distinct types:

    1. DTO (Data Transfer Object): The raw data format that comes from or goes to the backend.
    2. Model: Represents business logic. These are used as value objects and should not contain methods. Map DTOs to Models inside your HTTP services.
    3. VM (View Model): The specific data shape required by a component for rendering.
  10. Naming conventions for JavaScript/TypeScript functions

    master

    Follow specific prefix patterns based on the function's behavior to improve clarity:

    • Side effects (modifying state/arguments): Prefix with set. Example: setSomething(data: SomeType): void.
    • State changes (no arguments): Prefix with change. Example: changeSomething(): void.
    • Value retrieval: Prefix with get, regardless of whether arguments are present. Examples:
      • getSomething(): string
      • getSomething(data: SomeType): string
    getSometing() : string
    getSometing(data: SomeType) : string
  11. Use interfaces instead of Record for associative arrays

    master

    When creating an associative array where property names are known or specific, use an interface with an index signature rather than the Record<K, V> utility type. This allows you to explicitly name the property keys, which improves readability.

    // Instead of Record<number, SectionCounters>
    sectionsCounters: { [classifierValueId: number]: SectionCounters }
  12. Implement Immutability with `readonly` and `Readonly<X>`

    master

    To prevent unintended data mutations between parent and child components and to support OnPush change detection strategies, follow these immutability patterns:

    • Class/Interface Properties: Add the readonly modifier to all properties.
    • Object References: When you cannot set individual properties to readonly, use the Readonly<T> utility type to wrap the object.
    • Change Detection: When updating data, copy the object and change its reference rather than mutating existing properties.
    // Good: Using readonly on properties
    interface IRequirementDTO {
      readonly id: number;
      readonly name: string;
    }
    
    // Good: Using Readonly utility for objects
    requiremensDTO: Readonly<IRequirementDTO>[] = [];
    const user = Readonly<User>;