Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

This book includes some notes and best pratices of various web technologies, design patterns, architecture, and best practices, including:

  • Micro Services + Micro Frontend
  • RxJS
  • Angular
  • NgRx
  • Nx
  • TypeScript
  • Web foundations
  • Authentication
  • API Design

Some topics I encountered in my own web development. Others I learned/invesigated out of curiosity.

Architecture

Micro Services/Frontend

Micro Frontend

TL;DR

Micro Frontend is an architecture in which a monolithic front-end is decomposed into separate and independent apps. Each app has an independent build process and deployment, and even tech stack. So this indicates a faster development cycle.

Pros

Each MFE

  • has a small bundle size
  • can be built and deployed independently
  • can be lazy loaded
  • has minimum communication between other MFEs

Cons

  • Version mismatches where applications are deployed with different versions of shared libraries, which can lead to imcompatibility issues.

Use Cases

Micro Frontends are not like Micro-Services in the sense that they can run isolated. For example all your MFs share the window object, which opens up possibilities for conflicts.

We recommend MFE for teams that require applications to be deployed independently. It is important to consider the cost of MFEs and decide whether it makes sense for your own teams.

  • Version mismatches where applications are deployed with different versions of shared libraries, which can lead to incompatibility issues.
  • Independent deployments can lead to unexpected errors, such as any host-level changes to orchestration/coordination logic that breaks compatibility with remotes.

Deployment

Since deployments with MFEs are not atomic, there is a chance that shared libraries – both external (npm) and workspace – between the host and remotes are mismatched. The default the Nx setup configures all libraries as singletons, which requires that all affected applications be deployed for any given changeset, and makes à la carte deployments riskier. Core libraries such as react, angular, redux, ngrx, etc. must be singletons. Otherwise the applications will not work together.

There are mitigation strategies that can minimize mismatch errors. One such strategy is to share as little as possible between applications.

Best Practices

  1. different domain can use different representations for the same thing. If we tried to create a single model for both of these subsystems, it would be unnecessarily complex. It would also become harder for the model to evolve over time, because any changes will need to satisfy multiple teams working on separate subsystems. Therefore, it’s often better to design separate models that represent the same real-world entity (in this case, a drone) in two different contexts. Each model contains only the features and attributes that are relevant within its particular context.
  2. a microservice should be no smaller than an aggregate, and no larger than a bounded context
  3. an aggregate is a transactional boundary
  4. the Scheduler service sends an asynchronous message to the Supervisor, so that the Supervisor can schedule compensating transactions
  5. failure fallback during transaction
  6. independent deployability of micro frontends is key
  7. ? there could be a separate server responsible for rendering and serving each frontend
  8. against publish each micro front-end as a package
  9. Ensure that CSS is only applied to a specific front-end component via naming conventions
  10. communications between micro-fronts: we recommend having them communicate as little as possible, as it often reintroduces the sort of inappropriate coupling that we’re seeking to avoid in the first place.
  11. Use custom event to communicate indirectly via micro front-ends.
  12. Just like sharing a database across microservices, as soon as we share our data structures and domain models, we create massive amounts of coupling, and it becomes extremely difficult to make changes.
  13. The redux docs even mention “isolating a Redux app as a component in a bigger application” as a valid reason to have multiple stores.
  14. auth or other cross-cutting concerns should be owned by the container app.
  15. each micro front-end should has its own source repo and own deployment pipeline
  16. name collisions?
  17. extract dependencies? One approach is to externalise common dependencies from our compiled bundles. If there is a breaking change in a dependency, we might end up needing a big coordinated upgrade effort and a one-off lockstep release event. This is everything we were trying to avoid with micro frontends in the first place!
  18. define standards and conventions
  19. use of SSI?
  20. googd loading states for users?

Sharing the same database?

Microservices can share a database, but it is generally not recommended as a best practice.

Data model complexities: Different microservices might have different data requirements and structures. Sharing a database can lead to complex data models that are hard to manage and evolve.

Performance bottlenecks: If multiple microservices are accessing the same database, it can create contention and performance issues, especially during high loads.

Lack of independence: Changes to one microservice may have unintended consequences on other microservices that share the same database, leading to versioning and deployment challenges.

Reduced fault isolation: A bug or issue in one microservice could affect others that rely on the shared database, making it harder to pinpoint the root cause of the problem.

Sharing a state store between MFEs?

The general practice in using Redux is to have a single store, thereby having a single state object. This approach would mean that all the Micro Frontends would have a shared state. This is a violation of the Micro Frontend based architecture since each App is supposed to be a self-contained unit having its store.

In a Micro Frontend architecture, an individual application should not be able to modify the state of other apps. However, they should be able to see the state of other apps. Along the same line for enabling cross-application communication, they should also be able to send events/actions to other Stores and also get notified of changes in other apps’ state.

Communication between MFEs

IF we add providerIn:root to shared service, shell and remote mfe’s will initiate two instances of the services. Lazy loaded modules have their own root scope.

Custom Events using Browser’s API

Use the browser’s in-built custom event APIs to publish events with the data from one micro frontend. The other micro frontends subscribe to the events to get the data.

  • easy to scale
  • doesn’t work for mobile MFE

Custom Message Bus

Similar to one above, but instead of relying on browser’s custom events API, we build our own pub-sub mechanism.

Considerations

  1. only one database? based on microservices, it should have several?It’s much easier to perform schema updates, because only a single microservice is affected.
  2. decentralize everything. avoid sharing code or data schemas. Data storage should be private to the service that owns the data. Use the best storage for each service and data type. Avoid coupling between services. Causes of coupling include shared database schemas and rigid communication protocols.
  3. not working as team based on each microservice?
  4. micro-frontend: store is per microservice?
  5. how to define bounded context
  6. shell manages the messaging/routing?
  7. cross micro-front end communications?
  8. how can microservices be deployed with non-microfrontend
  9. Is each microservice/micro-frontend a separate codebase?
  10. Polyglot programming. Can some part of the code be written in Rust?
  11. Bounded context: it’s often better to design separate models that represent the same real-world identity in two different contexts.
  12. As a general principle, a microservice should be no smaller than an aggregate, and no larger than a bounded context.
  13. inter-service communication: sync API call or async messaging?
  14. is micro-front-end and microservice 1-on-1 relationship? for each front-end, the backend might be an aggregation of services
  15. team structure with micro-frontend and micro services
  16. what are the business domains?

In-Memory Cache

Use cases

  1. Speed up read-heavy database query
  2. Caching expensive computations
  3. Reducing calls to slow or rate-limited external APIs
  4. Rate limiting & abuse prevention

Redis cache

Redis is an in-memory data store (key–value) that’s commonly used as a cache. A Redis cache typically sits between your app and your database/external services:

  • App receives a request for data
  • Check Redis first
    • If found (cache hit): return quickly
    • If missing (cache miss): fetch from DB/API, then store in Redis with an expiration (TTL) This reduces database load and improves latency.

Redis vs DashMap

Redis becomes the better choice when you’re running multiple server instances. If you have two or more Axum processes behind a load balancer, an in-memory DashMap on each instance means their caches diverge immediately. Redis gives you a single shared cache that all instances read from, so every user sees the same state regardless of which server handles their request.

Dashmap vs moka

DashMap is a concurrent map. It gives you a thread-safe key-value store where multiple threads can read and write without a global lock (it uses internal sharding). You put things in, you get things out. Items stay there until you explicitly remove them.

Moka is a caching layer built on top of similar concurrency primitives, but with all the cache-specific behavior baked in: automatic eviction, TTL(time to live), TTI(time to idle), size bounds, and async loading.

The hybrid approach

A common production pattern is to layer both: Request -> In-memory (moka) -> Redis -> SeaORM/Postgres

  • L1 (in-memory): Sub-microsecond. Holds the hottest data — active rooms, online users, last ~100 messages per active room.
  • L2 (Redis): Shared across instances. Holds broader state, handles pub/sub for cross-instance fanout.
  • L3 (Database via ORM): Source of truth. Cold reads and persistence.

The thundering herd problem

Sometimes also called a cache stampede. The thundering herd problem in cache happens when many requests all miss the cache at the same time, then they all rush to rebuild the same data from the database or backend.

  • A hot cache key expires
  • 1000 users request that same data right after
  • Instead of 1 backend query, you now get 1000 backend queries
  • That sudden spike can overload your DB or upstream service

Server Side Data Models

Database

Sharding

Sharding is a database scaling technique that splits a large dataset horizontally across multiple database instances (called shards), each holding a subset of the data.

How it works:

  • A shard key (e.g., user ID, region) determines which shard stores a given record.
  • Each shard is an independent database that handles reads/writes for its partition of data.
  • A routing layer directs queries to the correct shard based on the key. For example: A users table with 10M rows sharded by user_id % 4 across 4 databases — shard 0 gets IDs 0,4,8…, shard 1 gets 1,5,9…, etc.

Benefits:

  • Horizontal scalability — add more shards as data grows
  • Better performance — each shard handles less data/traffic
  • Fault isolation — one shard failing doesn’t take down the whole system Trade-offs:
  • Cross-shard queries are expensive (joins across shards)
  • Rebalancing is complex when adding/removing shards
  • Operational overhead — more databases to manage
  • Hotspots can occur if the shard key distributes data unevenly

Sharding vs. Partitioning: Partitioning splits data within a single database; sharding splits it across multiple databases/servers. Sharding is essentially distributed partitioning.

Angular

Use of RxJS in Angular

There are various ways of using observables in Angular. Below are some of the patterns I’ve used:

  • rxjs/observables

    • three types of observables/subscription model:
      • Entity/state observable, directly subscribe in async pipe
      • Side effect observable, put side effects in the tap operator, and subscribe all at once:
      scheduled(
        [this.enabledPanelId$, this.onStatus$, this.refreshTime$],
        asyncScheduler
      )
        .pipe(mergeAll(), takeUntil(this.stop$))
        .subscribe();
      
      • UI event emitter and handler, use a combination of subject and async pipe
      • subscription in service
  • If accessing the store from the component constructor, consider adding:

    this.settingsMode$ = this.uiQuery?.settingsMode$.pipe(
      tap({
        next: (appMode) => {
          this.isPanelEditMode =
            appMode === SettingsModes.EDIT || appMode === SettingsModes.ADDNEW;
        },
      })
    );
    
  • Use dedicated view subscription variable:

    *ngIf="{ changeTheme: onChangeTheme$ | async, onSave: onSave$ | async, } as
    obs"
    

Peer Dependency

Peer dependencies is the dependencies of an npm package. Peer dependencies are declared by the library, and provided by the consuming application.

A library is more like a pure function that gives you an output with a set input. A library is not instantiated, nor can it stand on its own, this is also due to that library bundles do not include any of its dependencies. Instances of the dependencies will need to be provided by the consuming apps.

With peerDependencies you are required to download those packages yourself (the user consuming your library needs to download those packages, it doesn’t come bundled with your library).

Npm will not attempt to install peer dependencies when doing npm install.

Peer Dependency vs Dependency

In a library package.json, if a package is specified as a dependency instead of as a peer dependency, in the consuming app, npm will attempt to install it. This could easily end up with several verisons.

For example, a lib has a dependency of rxjs ^6.0.0, and consuming app has a dependency of rxjs ^7.0.0. Both versions will be installed, with v7 listed at the root of the node_module, and v6 listed inside of the node_module of the lib.

If however, the rxjs ^6.0.0 is listed as a peer dependency of the lib. Npm will throw an peer dependency error while running npm install, because the consuming app provides v7, not v6.

Recommendation from ng-packagr

Use peerDependencies whenever possible. As a rule of thumb, consider that your library’s dependencies are declared as peerDependencies. In most cases, this is the recommended solution for library dependencies.

Why? Publishing an npm packages with a dependencies section in package.json easily leads to installing multiple versions of a dependency to an application’s node_modules folder. While this is a desirable solution on server-side or standalone programs, it’s a source for bugs on front-end build stacks and UI technologies – you don’t want to install two different versions of Angular or RxJS.

Recommendation from Angular official doc

Angular libraries should list any @angular/* dependencies the library depends on as peer dependencies. This ensures that when modules ask for Angular, they all get the exact same module. If a library lists @angular/core in dependencies instead of peerDependencies, it might get a different Angular module instead, which would cause your application to break.

Two package.json files

When you create a library inside an Angular workspace. There will be two package.json files, one for the workspace root, one for the library itself.

  • my-workspace/package.json
  • my-workspace/projects/my-lib/package.json

When you specify dependencies for your library, you should specify peer dependencies in the library package.json.

When developping a library, you should install all peer dependencies of the library, inside the workspace package.json. Otherwise the library would not compile.

A note on dev dependency

You should not refer to any dev dependency inside your source code without allowing for your code to safely and gracefully fallback to alternatives since the package will be removed on pruning.

Entry Points

In Angular libraries, an entry point refers to a module or a set of modules that are exposed and can be imported by consumers of the library. There could be a single entry point versus multiple entry points. For smaller libraries, a single entry point might be appropriate, while larger libraries could benefit from multiple entry points to manage bundle size.

An Angular library with a single entry point is not tree-shakable by default. It is important to consider that when designing entry points. If bundle size is a concern, multiple entry points might be the way to go.

Single Entry Point:

In this approach, the library provides a single module that encapsulates all the functionality and components of the library. Consumers of the library only need to import one module to access all the features provided by the library. It simplifies the initial setup for consumers, as they only need to manage one import statement.

Advantages: Simplicity: Consumers have an easier time getting started, as they only need to import a single module. Consistency: The library maintains a unified and consistent API for all its features. Easier Maintenance: Internal changes within the library don’t impact consumers as long as the public API remains stable.

Disadvantages: Bundling Size: If the library contains a lot of features, a single entry point might lead to larger bundle sizes for consumers, even if they only use a subset of the features. Consumers who only need a small portion of the library’s features might end up including unused code in their bundles.

Multiple Entry Points:

In this approach, the library provides multiple modules, each corresponding to a specific feature or set of features. Consumers can import only the modules they need, reducing the risk of including unused code in their bundles. It allows for better fine-tuning of the bundle size, as consumers only import the necessary modules.

Advantages: Smaller Bundles: Consumers can import only the specific modules they need, leading to smaller bundle sizes. Optimized Performance: Reduced bundle sizes can lead to better application performance, especially in scenarios with limited bandwidth or slower networks.

Disadvantages: Complexity: Consumers need to manage multiple import statements for different modules, which can be more challenging, especially for larger libraries. Potential Duplication: If multiple modules share common dependencies, consumers might end up with duplicated code in their bundles.

Directives

In Angular, a directive is a building block used to extend the functionality of HTML elements or create reusable components. Directives allow you to attach specific behaviors or manipulate the DOM (Document Object Model) within your Angular application.

There are three types of directives in Angular:

Component Directives: Components are directives with a template. They consist of a view, which is the template HTML, and a class, which defines the behavior and properties of the component. Components are the most commonly used directive type in Angular.

Attribute Directives: Attribute directives are used to change the appearance or behavior of an existing element or component. They are applied by using an attribute on the HTML element. Examples of attribute directives in Angular include ngStyle and ngClass.

Structural Directives: Structural directives are used to change the structure of the DOM by adding or removing elements. They are denoted by an asterisk (*) preceding the directive attribute in the HTML markup. Examples of structural directives in Angular include *ngIf, *ngFor, and *ngSwitch.

ng-container

ng-container is a logical directive that allows you to group elements in a template but doesn’t itself get rendered in the DOM. One common use case of <ng-container> is alongside the *ngIf structural directive. By using the special element we can produce very clean templates easy to understand and work with.

Multiple structural directives cannot be used on the same element; if you need to take advantage of more than one structural directive, it is advised to use an <ng-container> per structural directive.

ng-template

If you add a ng-template tag to your template, it and everything inside it will be replaced by a comment. It can be used to define an else case:

<div>
  Hello word!
  <div *ngIf="false else content">Shouldnt be displayed</div>
</div>

<ng-template #content> Should be displayed </ng-template>

The <ng-template> element defines a block of content that a component can render based on its own logic. A component can get a reference to this template content, or TemplateRef, by using either the @ContentChild or @ContentChildren decorators.

Pipes

Standalone Pipes

  1. Import Directly When using a standalone pipe in a component or module, you should import it directly into the imports array of the component or module. This is similar to how you handle standalone components.
  2. Why Not Providers? Adding a pipe to the providers array is typically used for services, not for pipes. Pipes are used in templates and don’t require the providers array, since they don’t maintain any internal state or depend on Angular’s dependency injection.

Content Projection

Single slot

Use <ng-content> to project content into another component. Example:

import { Component } from "@angular/core";

@Component({
  selector: "app-zippy-basic",
  template: `
    <h2>Single-slot content projection</h2>
    <ng-content></ng-content>
  `,
})
export class ZippyBasicComponent {}

users of this component can project their own content

<app-zippy-basic>
  <p>Is content projection cool?</p>
</app-zippy-basic>

Multi-slot

You can add multiple contents slots and use a CSS selector to determine which ng-content tag to project your content into.

import { Component } from "@angular/core";

@Component({
  selector: "app-zippy-multislot",
  template: `
    <h2>Multi-slot content projection</h2>

    Default:
    <ng-content></ng-content>

    Question:
    <ng-content select="[question]"></ng-content>
  `,
})
export class ZippyMultislotComponent {}

and in consumer of the component:

<app-zippy-multislot>
  <p question>Is content projection cool?</p>
  <p>Let's learn about content projection!</p>
</app-zippy-multislot>

Conditional content projection

If your component needs to conditionally render content, or render content multiple times, you should configure that component to accept an <ng-template> element that contains the content you want to conditionally render.

Using an <ng-content> element in these cases is not recommended, because when the consumer of a component supplies the content, that content is always initialized, even if the component does not define an <ng-content> element or if that <ng-content> element is inside of an ngIf statement.

You can use ngTemplateOutlet directive to render a given <ng-template> element.

Template component:

<div *ngIf="expanded" [id]="contentId">
  <ng-container [ngTemplateOutlet]="content.templateRef"></ng-container>
</div>

Content component:

<ng-template appExampleZippyContent>
  It depends on what you do with it.
</ng-template>

Logic Angular will use when it encouters the appExampleZippyContent. This tells Angular to instantiate a template reference.

@Directive({
  selector: "[appExampleZippyContent]",
})
export class ZippyContentDirective {
  constructor(public templateRef: TemplateRef<unknown>) {}
}

Dependency Injection

Providers

A provider is an instruction to Angular’s dependency injection system on how to create or obtain a dependency (typically a service or value) that a class or component needs. Essentially, a provider tells Angular:

  • What to provide (the service, token, or value).
  • How to provide it (how to create or retrieve the dependency).

Providers in Angular form a hierarchical structure. When a component requests a dependency, Angular starts at the closest injector (typically the component itself) and works its way up through the hierarchy to find the right provider.

Injection Token

Previously, Angular only supported constructor-based dependency injection. However, things have changed with the inject function and we can now inject tokens and services outside of class constructors.

Injection Token vs Constants

Injection Token: Used to provide values or services that don’t have a class type.

  • Dynamic injection: Injection tokens are tied to Angular’s DI system, which means they can be injected into components, services, or other classes at runtime. This allows you to dynamically control or override the injected value depending on the context (e.g., by module, environment, testing).
  • Scoped to modules: With injection tokens, you can define values that can vary per module or per lazy-loaded part of the application. Different modules can provide different values for the same token.
  • Single value across the app: Constants are not tied to Angular’s DI scope, so their value is global and can’t be dynamically changed based on different modules or contexts.
  • Static and global: Constants are static values and don’t involve Angular’s DI system. Once defined, their value remains fixed throughout the app. They cannot be easily modified or replaced at runtime.

forRoot()

The forRoot() pattern helps to avoid a common problem in Angular: multiple instances of the same service being created across different modules. Here’s why it’s needed:

When a module is imported in multiple places (especially in lazy-loaded modules), Angular creates a new instance of the services declared in the module’s providers array. forRoot() allows you to centralize the service provisioning in the root module and only import the non-service parts (like components or directives) in feature modules, without duplicating the service.

Using Factory Methods for Injection Token

  • Dynamic Value Creation: The factory method allows the injected value to be created dynamically, based on runtime conditions.
  • Lazy Initialization: The value is only created when it’s first injected, not at the module’s startup.
  • Dependency Injection: The factory method itself can have dependencies, allowing for complex initialization logic that relies on other services or configuration values.
  • Conditional Logic: You can introduce conditional logic (such as different environments, user roles, or feature flags) to control what value is provided by the token.

Example:

export interface AppConfig {
  apiEndpoint: string;
  timeout: number;
}

// Define the factory method
export function configFactory(isProduction: boolean): AppConfig {
  return {
    apiEndpoint: isProduction ? "https://api.prod.com" : "https://api.dev.com",
    timeout: 5000,
  };
}

//Injection token
@NgModule({
  providers: [
    {
      provide: APP_CONFIG,
      useFactory: configFactory,
      deps: ["IS_PRODUCTION"], // Inject IS_PRODUCTION into the factory
    },
    { provide: "IS_PRODUCTION", useValue: true }, // Provide a value for IS_PRODUCTION
  ],
})
export class AppModule {}

//Use of Injection token
import { Inject, Component } from "@angular/core";

@Component({
  selector: "app-root",
  template: "<h1>{{ config.apiEndpoint }}</h1>",
})
export class AppComponent {
  constructor(@Inject(APP_CONFIG) public config: AppConfig) {
    console.log(this.config.apiEndpoint); // Outputs 'https://api.prod.com' if IS_PRODUCTION is true
  }
}

Keep Providers from Ending Up in the Wrong Injector

For example, if you use provideSvgIconsConfig in a component or a directive injector, you’ll get a compile error.

import { makeEnvironmentProviders } from "@angular/core";

export function provideSvgIconsConfig(config: Partial<SVG_CONFIG>) {
  return makeEnvironmentProviders([
    {
      provide: SVG_ICONS_CONFIG,
      useValue: config,
    },
  ]);
}

Injection Context

Injection token @Inject

When and Why to Use @Inject Custom Tokens:

  • If you’re using a string or symbol as a token (as shown in the example), you need to tell Angular what specific provider to inject. This is often used for injecting values or services that aren’t classes.
  • Multiple Implementations: If you have multiple providers that implement the same interface or provide the same class, you can use @Inject with different tokens to differentiate between them.
  • Abstract Classes or Interfaces: When using abstract classes or interfaces, which don’t exist at runtime, @Inject can be used with a custom token to specify the implementation to inject.`

@Injectable({ providedIn: 'root' })
class EventBusService1 implements EventBusService {
  // Implementation for EventBusService1
}

@Injectable({ providedIn: 'root' })
class EventBusService2 implements EventBusService {
  // Implementation for EventBusService2
}

// In your module or component providers
providers: [
  { provide: 'EVENT_BUS_SERVICE', useClass: EventBusService1 },
  { provide: 'ALTERNATIVE_EVENT_BUS_SERVICE', useClass: EventBusService2 }
]

// In your component
constructor(@Inject('EVENT_BUS_SERVICE') private eventBus: EventBusService) {
  // eventBus will be an instance of EventBusService1
}

constructor(@Inject('ALTERNATIVE_EVENT_BUS_SERVICE') private eventBus: EventBusService) {
  // eventBus will be an instance of EventBusService2
}

Unit Test

Mocking

  • Use mocks for all dependencies, instead of the real dependencies.
  • Recommend to use ng-mocks to quickly mock up dependencies.
  • Use ng-mocks together with Jest to customize mocking and spying for more complicated scenarios.

Anti-pattern

  • Avoid testing the round-trip. Test only the isolated piece.

Scenarios

  • Overwrite default mocking implementation within test: jest.spyon(myObject, 'someMethod').mockImplementation()
  • Use fakeAsync and tick from Angular testing utilities to deal with asynchronous code.
  • To test multiple subscriptions within ngOnInit, for each subscription, conditionally return a specific observable.
          jest.spyOn(dataService, 'sub').mockImplementationOnce((s, e) => {
          if (e === Events.ON_SUBMIT) {
            return of([1]); // Return an observable on Events.ON_SUBMIT event stream
          }
          return EMPTY; // Return an EMPTY on all other event streams
        });
    
    

Signals

  • A signal is a wrapper around a value that can notify interested consumers when that value changes. Signals can contain any value, from simple primitives to complex data structures.

  • Primary usage scenario of Signals: binding values reactively to the view.

  • A signal’s value is always read through a getter function, which allows Angular to track where the signal is used.

  • Signals may be either writable or read-only. Computed signals are not writable.

  • when working with signals that contain objects, sometime it’s useful to mutate object directly?

  • computed signals are both lazily evaluated and memorized. In the example below: doubleCount’s derivation function does not run to calculate its value until the first time doubleCount is read. Once calculated, this value is cached, and future reads of doubleCount will return the cached value without recalculating. When count changes, it tells doubleCount that its cached value is no longer valid, and the value is only recalculated on the next read of doubleCount.

    const count: WritableSignal<number> = signal(0);
    const doubleCount: Signal<number> = computed(() => count() * 2);
    
  • Therefore, it is safe to perform computationally expensive derivations in computed signals, such as filtering arrays.

  • Computed signal dependencies are dynamic.

  • An effect is an operation that runs whenever one or more signal values change. Effects always run at least once. When an effect runs, it tracks any signal value reads. Whenever any of these signal values change, the effect runs again.

  • Effects are rarely needed, but can be a good solution to logging, custom DOM behavior.

  • Avoid using effects for propagation of state changes.

  • The computed signal will run as many times as it was read. If the involved signals were not modified between the reads, computation will not be recomputed.

  • Will effect() run if a signal is updated but not changed? No, effect() is a consumer, and memoization works here as well.

  • When using signals in template, even if a computation itself will return the same value, it will still notify the consumer (in this case — the template), and it will still be recomputed (simply because we can not know the new value in advance). But if all the dependencies of this computed signal will return the same values, it will not notify the template, and, therefore, will not be recomputed.

  • Signals are glitch-free. If you change a signal several times in a row within a stack frame, only the last change will be seen by the consumer. This shows that Signals are not intended for modelling events but for data we want to bind to the view. In the cases we want to express events, observables are the way to go.

  • Signals are less suitable for asynchronous tasks and for representing events: Firstly, they do not offer an easy way to deal with overlapping asynchronous requests and the resulting race conditions. In addition, they cannot directly represent error states. Secondly, Signals ignore the resulting intermediate states when value changes occur in direct succession.

Lazy

Unlike Angular’s traditional change detection mechanism, signals do not automatically re-run whenever something in the component changes. They only react when their dependencies explicitly change, and even then, only when the signal’s value is accessed.

will only emit the value after the signal stabilizes?

const obs$ = toObservable(mySignal);
obs$.subscribe(value => console.log(value));
mySignal.set(1);
mySignal.set(2);
mySignal.set(3);

Change detection

With default change detection, there is no way for Angular to know exactly what has changed on the page, so that is why we cannot make any assumptions about what happened, and we need to check everything!

Because we have no guarantees of what could or could not have changed, we need to scan the whole component tree and all the expressions on every component.

Mutable Objects with Signals

Pitfall: Signals in Angular rely on reference checks to detect changes. If you use mutable objects within signals, updating the contents of these objects won’t trigger a reactivity change unless the reference itself changes. Solution: Favor immutability when working with signals. Instead of modifying an object in place, create a new object and set it to the signal.

const user = signal({ name: 'Alice', age: 30 });

// This won't trigger reactivity because the reference hasn't changed
user().age = 31;

// Correct approach
user.set({ ...user(), age: 31 });

Rules about computed()

Do not modify things in computed(). It should compute a new result, that’s it. Do not modify the DOM, do not mutate variables using this, and do not call functions that might do that. Do not push values to Observables — it will cause unintentional reactive context propagation (explained below for effect()). computed() should not have side effects, it should be a pure function.

Do not make asynchronous calls in computed(). This function does not allow modification of Signals (and it is amazingly helpful), but it can not track asynchronous code. Moreover, Angular Signals are strictly synchronous, so if you want to use asynchronous code in computed(), you are doing something wrong. So, no setTimeout(), no Promises, no other asynchronous things.

Rules about effect()

The function you provide to effect() should be as small as possible. This way it will be easier to read and spot erroneous behavior.

A best practice is to read signals first, then wrap the rest of the effect into untracked():

Effects needs an injection context to work. The technical reason is that effects use inject to get hold of the current DestroyRef. Typicallly setup your effects in the constructor.

Effects are not allowed to write signals.

Effects will execute minimal number of times: if an effect depends on multiple signals and several of them change at once, only one effect execution will be scheduled.

  • Will effect() run if a signal is updated but not changed?

two copies of everything…??

Computed Signals and Effects as a Replacement for Life Cycle Hooks

Life cycle hooks like ngOnInit and ngOnChanges can now be replaced with computed and effect:

markDownTitle = computed(() => '# ' + this.label())

constructor() {
  effect(() => {
    console.log('label updated', this.label());
    console.log('markdown', this.markDownTitle());
  });
}

using inputs within computed or effect is always safe, as they are only first triggered when the component has been initialized. Example:

@Component([...])
export class OptionComponent implements OnInit, OnChanges {
  label = input.required<string>();

  // safe
  markDownTitle = computed(() => '# ' + this.label())

  constructor() {
    // this would cause an exception,
    // as data hasn't been bound so far
    console.log('label', this.label);

    effect(() => {
        // safe
        console.log('label', this.label);
    })
  }

  ngOnInit() {
    // safe
    console.log('label', this.label);
  }

  ngOnChanges() {
    // safe
    console.log('label', this.label);
  }
}

Transform input

IMPORTANT: Do not use transforms if they change the meaning of the input, or if they are impure. Instead, use computed for transformations with different meaning, or an effect for impure code that should run whenever the input changes.

Benefits of signal inputs over @Input:

  • Signal inputs are more type safe.
  • Signal inputs, when used in templates, will automatically mark OnPush components as dirty.
  • Values can be easily derived whenever an input changes using computed.
  • Easier and more local monitoring of inputs using effect instead of ngOnChanges or setters.

Signals are Glitch-free

When writing code like in the previous section, we need to be aware that Signals are glitch-free. That means that if you change a signal several times in a row (within a stack frame), only the last change will be seen by the consumer, e.g. the effect:

@Component([...])
export class AboutComponent {

  constructor() {
    const signal1 = signal('A');
    const signal2 = signal('B');

    effect(() => {
      console.log('signal1', signal1());
      console.log('signal2', signal2());
    });

    signal1.set('C');
    signal1.set('D');

    signal1.set('E');

    signal2.set('F');
  }
}

In this case, we will only see the values E and F on the console. Indemediate values are skipped.

In cases where we want to express events, Observables are the way to go, as they don’t have this glitch-free guarantee by design.

Signals and Observables

  • Every variable (that might change) in your new templates should be a Signal.
  • If the role of the variable can be described as conditions, then use Signal; if the description of the variable includes time, you need an observable. Signals has no time axis.
  • If you need some value from an Observable in your computed(), create a Signal using toSignal() (outside of computed()).
  • If you need to read a signal in Observables’s pipe():
    • If you need to react to changes in that signals in your observable, convert the signal into observable and add it using some join operator
    • If you just need to current value of the signal, read a signal directly in your operators. Observable is not a reactive context, no need to untracked().
    • However, when discussing reading signals inside the observable in general, there’s one case where we should use untracked(). For this case to occur, two conditions must be met:
      • We are in a reactive context before the subscribe() call (the template calls our function, or our function is inside computed() or effect()).
      • The observable emits a value synchronously (for example, if getData() decides to return cached data).
  • Signals have no “complete” state
  • One of the important differences between signals and observables is that signals do not send their new value to their consumers when their value is modified. Instead, they simply notify their consumers that the value has been modified. To fetch the new value of a signal, the consumer should “read” that signal. Derived signals (created with computed()) produce new values by executing their computations. And only the consumer decides when to do this.

Reactivity Context

The body of the function passed to computed(), effect() or template is called “reactivity context”. Observable is not a reactive context.

Reading a signal outside the watching function

const title = message.$title();

watcher(() => {
  console.log(title);
});

message.updateTitle('Bar');

Here, title is not a signal. It’s the value we’ve read outside of the watching function, so watcher() will not be notified when we update the signal.

Changing a non-observable reference

watcher(() => {
  console.log(message.$title());
});

message = new Message('Bar', 'Martijn', ['Felicia', 'Marcus']);

Here, we replace the message, but watcher() uses the reference to another variable, and it will not be notified that we’ve replaced the reference.

Dependency Tracking

Dependency Tracking in Angular Signals is recursive for synchronous function calls. Producers, consumed asynchronously, will not be registered.

ExpressionHasChangedAfterChecked

Control Flow

@let

@let declarations are scoped to the current view and its descendants. Since they are not hoisted, they cannot be accessed by parent views or siblings:

Server-side Rendering

SSR-friendly

When something is SSR-friendly, it means it:

  • Runs without browser-only APIs (like window, localStorage, etc.)
  • Can run on the server and the client without issues
  • Can hydrate correctly — meaning:
    • HTML rendered on the server matches what’s rehydrated in the browser
    • No UI flickering or double-fetching

Client hydration

Hydration is the process where the browser takes the static HTML rendered by the server and turns it into a fully interactive Angular app. Steps:

  1. Server renders your Angular component into HTML.
  2. That HTML is sent to the browser and displayed instantly — this is called First Contentful Paint (FCP).
  3. Angular’s runtime downloads the JavaScript bundle, boots up, and “hydrates” the existing HTML:
    • Connects event listeners
    • Initializes signals or reactive data
    • Makes the page interactive

Issues with hydration

  1. Mismatches between server and client If the HTML generated on the server doesn’t match what Angular renders on the client, you get hydration errors or visual flickering.

  2. Double-fetching

    If you don’t use TransferState or HttpResources, the client re-fetches data that was already fetched on the server — bad for performance.

The double-fetching problem

When you use HttpClient + RxJS without transfer state, Angular doesn’t know the server already fetched the data, so it re-runs the HttpClient calls on the client during hydration.

This causes:

  • Duplicate HTTP requests - one on the server, one again on the client.
  • Flash of loading states: UI might flicker: HTML shows data → browser loads → shows “Loading…” → then shows data again.
  • Wasted bandwidth/performance. Unnecessary API calls and CPU usage

HttpResources

It integrates with Angular’s transfer state mechanism. When used with provideHttpResources(), it:

  • Fetches data on the server
  • Embeds that data in the HTML response
  • Prevents duplicate HTTP requests when the client bootstraps

This means:

  • No flash of loading state
  • No duplicate fetch on the client
  • Seamless hydration

When not to use HttpResources

  • If your fetch should only happen under very specific conditions (e.g., after user clicks a button), HttpResources’ eagerness can become a problem.
  • HttpResources is designed primarily for fetching (i.e., GET). It doesn’t have built-in support for POST, PUT, DELETE with side effects, complex transaction logic, advanced request chaining, low-level control (custom headers, retries, timeout, events).
  • It doesn’t support cross-component or global cache coordination.

Best Practices

General

  • Follow the Single Responsibility Principle (SRP): Keep your components focused on a single responsibility. Split complex components into smaller, reusable components to improve code maintainability and reusability.
  • Leverage reactive programming using RxJS to handle async operations, manage state, and handle event streams.
  • Use modules
  • Minimize DOM Manipulation: Directly manipulating the DOM can be costly in terms of performance. Utilize Angular’s data binding and structural directives (e.g., ngFor, ngIf) to update the DOM efficiently.
  • Optimize Change Detection: Angular’s change detection is powerful but can impact performance. Use ChangeDetectionStrategy.OnPush where possible to enable change detection optimizations. This strategy helps reduce the number of checks performed by the Angular change detection mechanism.
  • Use simple scalable architecture:
    • core: eager layout, logic, app wide state
    • features: lazy features
    • shared: reusable components, directives, pipes

Libraries

  • Declarations such as components and pipes should be designed as stateless, meaning they don’t rely on or alter external variables.
  • Use peerDependencies whenever possible. Publishing an npm packages with a dependencies section in package.json easily leads to installing multiple versions of a dependency to an application’s node_modules folder. While this is a desirable solution on server-side or standalone programs, it’s a source for bugs on front-end build stacks and UI technologies – you don’t want to install two different versions of Angular or RxJS.
  • Angular libraries should list any @angular/* dependencies the library depends on as peer dependencies. This ensures that when modules ask for Angular, they all get the exact same module. If a library lists @angular/core in dependencies instead of peerDependencies, it might get a different Angular module instead, which would cause your application to break.
  • Peer dependency requirements, unlike those for regular dependencies, should be lenient. You should not lock your peer dependencies down to specific patch versions.

Lightweight Injection Tokens

RxJS

RxJS is a library for composing asynchronous and event-based programs by using observable sequences. It provides one core type, the Observable, satellite types (Observer, Schedulers, Subjects) and operators to allow handling asynchronous events.

Foundations

  • “Calling” or “subscribing” is an isolated operation: two function calls trigger two separate side effects, and two Observable subscribes trigger two separate side effects. As opposed to EventEmitters which share the side effects and have eager execution regardless of the existence of subscribers, Observables have no shared execution and are lazy.

  • Observables are lazy Push collections of multiple values.

  • Subscribing to an Observable is analogous to calling a Function.

  • the subscription of observables was entirely synchronous, just like a function

  • Observables are able to deliver values either synchronously or asynchronously

  • Observables can “return” multiple values over time, something which functions cannot.

  • Observables can be created with new Observable. Most commonly, observables are created using creation functions, like of, from, interval, etc.

  • When you subscribe, you get back a Subscription, which represents the ongoing execution. Just call unsubscribe() to cancel the execution.

  • RxJS is mostly useful for its operators, even though the Observable is the foundation.

  • Observers are simply a set of callbacks, one for each type of notification delivered by the Observable: next, error, and complete.

  • A Pipeable Operator is essentially a pure function which takes one Observable as input and generates another Observable as output. Subscribing to the output Observable will also subscribe to the input Observable.

  • Observables most commonly emit ordinary values like strings and numbers, but surprisingly often, it is necessary to handle Observables of Observables, so-called higher-order Observables

  • how do you work with a higher-order Observable? Typically, by flattening: by (somehow) converting a higher-order Observable into an ordinary Observable.

    const fileObservable = urlObservable.pipe(
      map((url) => http.get(url)),
      concatAll()
    );
    

Subject and multi-cast

  • An RxJS Subject is a special type of Observable that allows values to be multicasted to many Observers. While plain Observables are unicast (each subscribed Observer owns an independent execution of the Observable), Subjects are multicast.
  • A Subject is like an Observable, but can multicast to many Observers. Subjects are like EventEmitters: they maintain a registry of many listeners.
  • BehaviorSubject requires an initial value and emits the current value to new subscribers. BehaviorSubject are guaranteed to emit synchronously.
  • BehaviorSubject: Imagine going to a movie late and asking your friend, “Hey, what just happened?” and they fill you in. BehaviorSubject is similar: when you subscribe, it will give you the latest value that was emitted before you tuned in, and then you continue getting updates. So, if you need that “previous context,” this is your pick. BehaviorSubject also require a seed value.
  • ReplaySubject: Think of this as a DVR for your TV. It records, say, the last 5 shows, and you can replay those whenever you switch on your TV. ReplaySubject can keep a buffer of emitted values, and when you subscribe, it will “replay” those values for you, ensuring you don’t miss out on what was broadcasted earlier.
  • In contrast, a simple Subject doesn’t offer these playback features. If you join late, you’ve missed it, akin to a live concert. You only hear what’s played after you’ve arrived.
  • In summary, choose Subject for standard multicasting needs. Opt for BehaviorSubject when the latest value or seed value is critical, and ReplaySubject when you want to ensure a history of values is available for late subscribers.
  • In RxJS, “single cast” and “multicast” refer to two different ways of handling the emission of values from an Observable to its subscribers.
  • Single cast: When an Observable is single-cast, it means that each subscription to that Observable triggers a separate execution of its underlying logic. In other words, each subscriber gets its own independent stream of values.
  • Multicast: When an Observable is multicast, it means that it shares a single execution of its underlying logic among multiple subscribers. This is achieved using a special type of Observable called a “Subject” or a “Subject-like” entity.
  • refCount makes the multicasted Observable automatically start executing when the first subscriber arrives, and stop executing when the last subscriber leaves.
  • scheduler: In RxJS, a scheduler is an abstraction that provides a way to control the execution and timing of Observable streams. It allows you to specify when and how the values emitted by an Observable are delivered to the subscribers.

Pure Functions

Characteristics of a Pure function:

  • Referential Transparency: A pure function always produces the same output for the same input, regardless of when or where it is called in a program. In other words, the output of a pure function depends solely on its input and has no dependency on any external state or side effects.
  • Lack of Side Effects: A pure function does not modify or affect the state of variables outside its local scope. It does not have any observable side effects, such as modifying global variables, writing to a file, or making network requests. The only result of calling a pure function is the computed return value.

Purity in RxJS:

Normally you would create an impure function, where other pieces of your code can mess up your state.

let count = 0;
document.addEventListener("click", () =>
  console.log(`Clicked ${++count} times`)
);

Using RxJS you isolate the state.

import { fromEvent, scan } from "rxjs";

fromEvent(document, "click")
  .pipe(scan((count) => count + 1, 0))
  .subscribe((count) => console.log(`Clicked ${count} times`));

Useful Operators

Useful RxJS Operators:

  • scan: With each emitted value, the accumulator function is applied, and the accumulated result is emitted instantaneously. You can remember this by the phrase “accumulate and emit on-the-go.”

  • reduce: However, be cautious when using scan for cases where the only the final accumulated result is crucial. In those situations, the reduce operator may be more appropriate, as it emits only the final value after the source completes.

  • trottleTime: Emit first value then ignore for specified duration

  • sampleTime: sampleTime periodically captures the most recent value at regular intervals, regardless of whether there is a new emission or not.

  • debounceTime: debounceTime delays the emission of values until a pause of specified time occurs, discarding intermediate values

  • exhaustAll subscribes to an Observable that emits Observables, also known as a higher-order Observable. Each time it observes one of these emitted inner Observables, the output Observable begins emitting the items emitted by that inner Observable.

  • switchMap: Maps each value to an Observable, then flattens all of these inner Observables using switchAll. The main difference between switchMap and other flattening operators is the cancelling effect. On each emission the previous inner observable (the result of the function you supplied) is cancelled and the new observable is subscribed. You can remember this by the phrase switch to a new observable.

  • switchMap is generally considered a safer default to mergeMap. Be careful though, you probably want to avoid switchMap in scenarios where every request needs to complete, think writes to a database. switchMap could cancel a request if the source emits quickly enough. In these scenarios mergeMap is the correct option.

  • concatMap Warning: if source values arrive endlessly and faster than their corresponding inner Observables can complete, it will result in memory issues as inner Observables amass in an unbounded buffer waiting for their turn to be subscribed to. Note: concatMap is equivalent to mergeMap with concurrency parameter set to 1.

  • mergeAll: Converts a higher-order Observable into a first-order Observable which concurrently delivers all values that are emitted on the inner Observables.

  • If you need to merge multiple observables that rely on each other for calculations or decisions, combineLatest may be more suitable.

  • If you’re working with observables that only emit one value or you only require the last value of each before completion, forkJoin is likely a better choice.

  • If you need to merge observables that produce values independently and are short-lived, mergeAll is the operator to reach for.

  • One common use case for this is if you wish to issue multiple requests on page load (or some other event) and only want to take action when a response has been received for all. In this way it is similar to how you might use Promise.all.

  • iif: Checks a boolean at subscription time, and chooses between one of two observable sources

  • groupBy: Group objects by id and return as array.

    import { of, groupBy, mergeMap, reduce } from "rxjs";
    
    of(
      { id: 1, name: "JavaScript" },
      { id: 2, name: "Parcel" },
      { id: 2, name: "webpack" },
      { id: 1, name: "TypeScript" },
      { id: 3, name: "TSLint" }
    )
      .pipe(
        groupBy((p) => p.id),
        mergeMap((group$) => group$.pipe(reduce((acc, cur) => [...acc, cur], [])))
      )
      .subscribe((p) => console.log(p));
    
    // displays:
    // [{ id: 1, name: 'JavaScript' }, { id: 1, name: 'TypeScript'}]
    // [{ id: 2, name: 'Parcel' }, { id: 2, name: 'webpack'}]
    // [{ id: 3, name: 'TSLint' }]
    
  • distinctUntilChanged Returns a Observable that emits all values pushed by the source observable if they are distinct in comparison to the last value the result observable emitted.

    of(1, 1, 1, 2, 2, 2, 1, 1, 3, 3)
      .pipe(distinctUntilChanged())
      .subscribe(console.log);
    // Logs: 1, 2, 1, 3
    
  • catchError: Catches errors on the observable to be handled by returning a new observable or throwing an error.

  • retry

    const result = source.pipe(
      mergeMap((val) => (val > 5 ? throwError(() => "Error!") : of(val))),
      retry(2) // retry 2 times on error
    );
    
  • shareReplay: You generally want to use shareReplay when you have side-effects or taxing computations that you do not wish to be executed amongst multiple subscribers. It may also be valuable in situations where you know you will have late subscribers to a stream that need access to previously emitted values. This ability to replay values on subscription is what differentiates share and shareReplay.

import { timer } from 'rxjs';
import { tap, mapTo, share } from 'rxjs/operators';

//emit value in 1s
const source = timer(1000);
//log side effect, emit result
const example = source.pipe(
  tap(() => console.log('***SIDE EFFECT***')),
  mapTo('***RESULT***')
);

/*
  ***NOT SHARED, SIDE EFFECT WILL BE EXECUTED TWICE***
  output:
  "***SIDE EFFECT***"
  "***RESULT***"
  "***SIDE EFFECT***"
  "***RESULT***"
*/
const subscribe = example.subscribe(val => console.log(val));
const subscribeTwo = example.subscribe(val => console.log(val));

//share observable among subscribers
const sharedExample = example.pipe(share());
/*
  ***SHARED, SIDE EFFECT EXECUTED ONCE***
  output:
  "***SIDE EFFECT***"
  "***RESULT***"
  "***RESULT***"
*/
const subscribeThree = sharedExample.subscribe(val => console.log(val));
const subscribeFour = sharedExample.subscribe(val => console.log(val));

Comparisons

forkJoin vs combineLatest

forkJoin require all input observables to be completed, but it also returns an observable that produces a single value that is an array of the last values produced by the input observables. In other words, it waits until the last input observable completes, and then produces a single value and completes.

In contrast, combineLatest returns an observable that produces a new value every time the input observables do, once all input observables have produced at least one value. This means it could have infinite values and may not complete. It also means that the input observables don’t have to complete before producing a value.

Best Practices

General

  • Avoid nested subscriptions: Nested subscriptions can lead to memory leaks and make your code harder to understand and maintain. Instead, use higher-order operators like switchMap, mergeMap, or concatMap to flatten and manage inner observables.
  • Use Subjects judiciously: Subjects can be powerful, but they should be used carefully. Avoid using Subjects as a default solution for sharing data between components. Consider using BehaviorSubject or ReplaySubject when you need to share state or emit initial values.
  • Leverage error handling: RxJS provides operators like catchError and retry to handle errors gracefully. Use these operators to handle errors at the appropriate level in your observable chain and provide meaningful error messages to users.
  • Use AsyncPipe when possible: In Angular templates, prefer using the AsyncPipe to subscribe to observables and handle the subscriptions automatically. This helps manage subscriptions and simplifies the template code.

Avoid Race Conditions

Overlapping asynchronous operations usually lead to undesirable race conditions. For example, if the user searches for two different desserts in quick succession, both results might be displayed one after the other. One of the two only flashes briefly before the other replaces it. Due to the asynchronous nature, the order of results does not need to match the order of requests.

To prevent this confusing behavior, RxJS provides various flattening operators:

  • switchMap
  • mergeMap
  • concatMap
  • exhaustMap

NgRx

NgRx is a reactive state management library for Angular applications inspired by Redux. It leverages RxJS to handle state changes and provides a predictable state container. NgRx follows a unidirectional data flow, where actions trigger state changes through reducers, and components subscribe to the state to update their views.

In NgRx, you typically define a separate reducer function for each slice of state in the store, rather than for every individual field. Each reducer is responsible for managing a specific portion of the overall state.

NgRx isolates side effects to promote a cleaner component architecture.

Actions

In the context of NgRx, “indirection” refers to the practice of abstracting away direct interactions with the state or actions, often through the use of selectors, action creators, or effects. This concept allows for more modular, maintainable, and testable code by decoupling the business logic from the UI components and providing a layer of abstraction.

  • Event-Driven - capture events not commands as you are separating the description of an event and the handling of that event.

  • Action describes the event, reducer handles the events

  • Upfront - write actions before developing features to understand and gain a shared knowledge of the feature being implemented.

  • Actions are inexpensive to write, so the more actions you write, the better you express flows in your application.

  • The createActionGroup function returns a dictionary of action creators where the name of each action creator is created by camel-casing the event name, and the action type is created using the “[Source] Event Name” pattern.

  • Define actions by automatically using a camel case event name, instead of string of event names.

    import { createActionGroup, props } from "@ngrx/store";
    
    import { Product } from "./product.model";
    
    export const ProductsApiActions = createActionGroup({
      source: "Products API",
      events: {
        productsLoadedSuccess: props<{ products: Product[] }>(),
        productsLoadedFailure: props<{ errorMsg: string }>(),
      },
    });
    

Reducers

Reducers are pure functions in that they produce the same output for a given input. They are without side effects and handle each state transition synchronously.

  • “I need to Remove a state value if another state part has changed. How should I access to other store parts from a reducer in NgRx?” in this case, recommended to dispatch a new action and handle via effects

  • Use meta-reducers as middleware to log or debug. Example:

    // console.log all actions
    export function debug(reducer: ActionReducer<any>): ActionReducer<any> {
      return function (state, action) {
        console.log("state", state);
        console.log("action", action);
        return reducer(state, action);
      };
    }
    
    export const metaReducers: MetaReducer<any>[] = [debug];
    
    @NgModule({
      imports: [StoreModule.forRoot(reducers, { metaReducers })],
    })
    export class AppModule {}
    

Selectors

  • @ngrx/store keeps track of the latest arguments in which your selector function was invoked. Because selectors are pure functions, the last result can be returned when the arguments match without reinvoking your selector function. This can provide performance benefits, particularly with selectors that perform expensive computation. This practice is known as memoization.

  • An advanced technique is to combine selectors with RxJS pipeable operators. The select method can be used inside the pipe operator. Example:

    import { select } from "@ngrx/store";
    import { map, filter } from "rxjs/operators";
    
    store
      .pipe(
        select(selectValues),
        filter((val) => val !== undefined)
      )
      .subscribe(/* .. */);
    

    -To make the select() and filter() behaviour a reusable piece of code, we extract a pipeable operator using the RxJS pipe() utility function:

    import { select } from "@ngrx/store";
    import { pipe } from "rxjs";
    import { filter } from "rxjs/operators";
    
    export const selectFilteredValues = pipe(
      select(selectValues),
      filter((val) => val !== undefined)
    );
    
    store.pipe(selectFilteredValues).subscribe(/* .. */);
    
  • Advanced exmaple: select the last {n} state transitions, a combination of NgRx and RxJS operators:

    • selector function from the state

      export const selectProjectedValues = createSelector(
        selectFoo,
        selectBar,
        (foo, bar) => {
          if (foo && bar) {
            return { foo, bar };
          }
      
          return undefined;
        }
      );
      
    • component should visualize the history of state transitions. We are not only interested in the current state but rather like to display the last n pieces of state. Meaning that we will map a stream of state values (1, 2, 3) to an array of state values ([1, 2, 3]).

      // The number of state transitions is given by the user (subscriber)
      export const selectLastStateTransitions = (count: number) => {
        return pipe(
          // Thanks to `createSelector` the operator will have memoization "for free"
          select(selectProjectedValues),
          // Combines the last `count` state values in array
          scan((acc, curr) => {
            return [curr, ...acc].filter(
              (val, index) => index < count && val !== undefined
            );
          }, [] as { foo: number; bar: string }[]) // XX: Explicit type hint for the array.
          // Equivalent to what is emitted by the selector
        );
      };
      
    • subscribe to in component and provide the number n

      // Subscribe to the store using the custom pipeable operator
      store.pipe(selectLastStateTransitions(3)).subscribe(/* .. */);
      
  • The key difference between a selector and a feature selector is that selectors are used to retrieve and transform state from the store in a general sense, whereas feature selectors are specifically used to work with feature state, which is state associated with a particular feature module in your application.

  • By using feature selectors, you can access and manipulate the state specific to a feature module without worrying about other parts of the application state, providing a more modular and focused approach to state management in NgRx.

  • Using feature selector vs state.feature, it allows you to compose other more complicated selectors using a feature selector.

Effects

  • Effects isolate side effects from components, allowing for more pure components that select state and dispatch actions.

  • Effects are long-running services that listen to an observable of every action dispatched from the Store.

  • Effects filter those actions based on the type of action they are interested in. This is done by using an operator.

  • Effects perform tasks, which are synchronous or asynchronous and return a new action.

  • In a traditional service based application, the component has to use service to perform a side-effect (reaching out to an external API to fetch movies), change the state of the movies within the component.

  • Effects when used along with Store, decrease the responsibility of the component.

  • Effects handle external data and interactions, allowing your services to be less stateful and only perform tasks related to external interactions, react to store state changes,

  • Effects can listen to not only actions, but any RxJs stream. This can handle periodic things like (refreshing of an auth token, uploading of logs), or reacting to user interaction.

  • Effects contains these parts: an action$ observable stream, ofType operator to filter which action to listen to,

  • Example:

    login$ = createEffect(() =>
      this.actions$.pipe(
        ofType(LoginPageActions.login),
        map((action) => action.credentials),
        exhaustMap((auth: Credentials) =>
          this.authService.login(auth).pipe(
            map((user) => AuthApiActions.loginSuccess({ user })),
            catchError((error) => of(AuthApiActions.loginFailure({ error })))
          )
        )
      )
    );
    
  • You can create a functional effect outside a class, using the functional: true flag. If the dispatch: false flag is set, the effect doesn’t return actions.

    import { inject } from "@angular/core";
    import { catchError, exhaustMap, map, of, tap } from "rxjs";
    import { Actions, createEffect, ofType } from "@ngrx/effects";
    
    import { ActorsService } from "./actors.service";
    import { ActorsPageActions } from "./actors-page.actions";
    import { ActorsApiActions } from "./actors-api.actions";
    
    export const loadActors = createEffect(
      (actions$ = inject(Actions), actorsService = inject(ActorsService)) => {
        return actions$.pipe(
          ofType(ActorsPageActions.opened),
          exhaustMap(() =>
            actorsService.getAll().pipe(
              map((actors) => ActorsApiActions.actorsLoadedSuccess({ actors })),
              catchError((error: { message: string }) =>
                of(
                  ActorsApiActions.actorsLoadedFailure({
                    errorMsg: error.message,
                  })
                )
              )
            )
          )
        );
      },
      { functional: true }
    );
    
    export const displayErrorAlert = createEffect(
      () => {
        return inject(Actions).pipe(
          ofType(ActorsApiActions.actorsLoadedFailure),
          tap(({ errorMsg }) => alert(errorMsg))
        );
      },
      { functional: true, dispatch: false }
    );
    
  • Effects start running immediately after instantiation to ensure they are listening for all relevant actions as soon as possible.

  • If additional metadata is needed to perform an effect besides the initiating action’s type, we should rely on passed metadata from an action creator’s props method.

  • However, there may be cases when the required metadata is only accessible from state. When state is needed, the RxJS withLatestFrom or the @ngrx/effects concatLatestFrom operators can be used to provide it. Example:

    addBookToCollectionSuccess$ = createEffect(
      () =>
        this.actions$.pipe(
          ofType(CollectionApiActions.addBookSuccess),
          concatLatestFrom((action) =>
            this.store.select(fromBooks.getCollectionBookIds)
          ),
          tap(([action, bookCollection]) => {
            if (bookCollection.length === 1) {
              window.alert("Congrats on adding your first book!");
            } else {
              window.alert("You have added book number " + bookCollection.length);
            }
          })
        ),
      { dispatch: false }
    );
    
  • Using other observable sources for effects. Example: creating an effect that listens to the document click event:

    trackUserActivity$ = createEffect(
      () =>
        fromEvent(document, "click").pipe(
          concatMap((event) => this.userActivityService.trackUserActivity(event))
        ),
      { dispatch: false }
    );
    

Router Store

The purpose of @ngrx/router-store is to synchronize the Angular Router state with your application’s NgRx store. During each router navigation cycle, multiple actions are dispatched that allow you to listen for changes in the router’s state. You can then select data from the state of the router to provide additional information to your application.

Entity

Entity provides an API to manipulate and query entity collections.

  • Provides performant CRUD operations for managing entity collections.
  • Extensible type-safe adapters for selecting entity information. Example:
/**
 * @ngrx/entity provides a predefined interface for handling
 * a structured dictionary of records. This interface
 * includes an array of ids, and a dictionary of the provided
 * model type by id. This interface is extended to include
 * any additional interface properties.
 */
export interface State extends EntityState<Book> {
  selectedBookId: string | null;
}

/**
 * createEntityAdapter creates an object of many helper
 * functions for single or multiple operations
 * against the dictionary of records. The configuration
 * object takes a record id selector function and
 * a sortComparer option which is set to a compare
 * function if the records are to be sorted.
 */
export const adapter: EntityAdapter<Book> = createEntityAdapter<Book>({
  selectId: (book: Book) => book.id,
  sortComparer: false,
});

/**
 * getInitialState returns the default initial state
 * for the generated entity state. Initial state
 * additional properties can also be defined.
 */
export const initialState: State = adapter.getInitialState({
  selectedBookId: null,
});

export const reducer = createReducer(
  initialState,
  /**
   * The addMany function provided by the created adapter
   * adds many records to the entity dictionary
   * and returns a new state including those records. If
   * the collection is to be sorted, the adapter will
   * sort each record upon entry into the sorted array.
   */
  on(
    BooksApiActions.searchSuccess,
    CollectionApiActions.loadBooksSuccess,
    (state, { books }) => adapter.addMany(books, state)
  ),
  /**
   * The addOne function provided by the created adapter
   * adds one record to the entity dictionary
   * and returns a new state including that records if it doesn't
   * exist already. If the collection is to be sorted, the adapter will
   * insert the new record into the sorted array.
   */
  on(BookActions.loadBook, (state, { book }) => adapter.addOne(book, state)),
  on(ViewBookPageActions.selectBook, (state, { id }) => ({
    ...state,
    selectedBookId: id,
  }))
);

NgRx + Signals

Signal Store

A SignalStore is created using the signalStore function. This function accepts a sequence of store features.

Signals are not meant to have a concept of time. Also, the effect is somewhat tied to Angular change detection, so you can’t observe every action that would be dispatched over time through some sort of Signal API. The global NgRx Store is still the best mechanism to dispatch action(s) over time and react to them across multiple features.

When a reactive method is called with a signal, the reactive chain is executed every time the signal value changes. Example:

import { Component, OnInit, signal } from '@angular/core';
import { map, pipe, tap } from 'rxjs';
import { rxMethod } from '@ngrx/signals/rxjs-interop';

@Component({ /* ... */ })
export class NumbersComponent implements OnInit {
  readonly logDoubledNumber = rxMethod<number>(
    pipe(
      map((num) => num * 2),
      tap(console.log)
    )
  );

  ngOnInit(): void {
    const num = signal(10);
    this.logDoubledNumber(num);
    // console output: 20
    
    num.set(20);
    // console output: 40
  }
}

Best Practices

  • Consistent folder strucutre: separate folders for actions, reducers, effects.
  • Keep actions simple
  • Reducers should be pure
  • Maintain immutability
  • Use effects for side effects
  • Avoid excessive nesting of states
  • Effects are for orchestration only, make sure that the actual logic is in services to keep the effects clean.
  • Keep effects small and modular as possible.
  • Use @ngrx/schematics to auto generate feature
  • The MOST GENIUS things about NgRx is that whole 80% of logic we have to write is just plain TypeScript functions which are NOT aware of Angular or RxJs. 80% are in the form of pure functions.
  • When we need to access NgRx state slice from one lazy feature in another lazy feature of our Angular application. Importing stuff between sibling lazy features is forbidden because it would break all the benefits of such architecture and could in theory lead also to runtime errors
  • componenet should only do two things in the context of ngrx:
    • retrieve and display state from the store using NgRx selectors
    • dispatch actions based on user interactions
  • In effects, use catchError operator to handle error
  • Avoid saving derived state in state object: A common problem with NgRx and other state management libraries is saving derived state in your state object. The derived state is often updated via the reducer as a result of a change in another part of the state. This can cause the derived state to get out of sync if any of the reducers forgets to also update the derived state. That is the reason why NgRx recommends always accessing the derived state via selectors instead of saving it in the state object. However, if performance is an issue, you can try to save a minimal derived state in store.

Nx

Module Federation

withModuleFederation function

withModuleFederation is used in the webpack config. This function is an abstraction on top of webpack’s ModuleFederationPlugin with some Nx-specific behavior.

  • All libraries (npm and workspace) are shared singletons by default, so you don’t manually configure them.
  • Remotes are referenced by name only, since Nx knows which ports each remote is running on (in development mode).

nx serve

When a developer runs say nx serve host --devRemotes=cart, they still run the whole application, but shop and about are served statically, from cache. As a result, the serve time and the time it takes to see the changes on the screen go down, often by an order of magnitude.

MFE

Nx provides linting rules. Once in place, they give us errors when we directly reference code belonging to another Micro Frontend.

TypeScript

Type, Interface and Class

Record vs Map

Contrary to a Record which is a type, a Map is a full-fledged data structure for storing the key-value pairs. It provides dedicated methods for adding, retrieving, and deleting entries from the Map.

Record<string, string>

Record<string, string> is semantically equivalent to index signature

{
  [key: string]: string;
};

However, with the index signature, one can customize it further (e.g., restrict certain key names or mix other properties), while Record is strictly a mapping.

type Example3 = {
  [key: string]: string;
  specialKey?: number; // Adds a specific key with its own type
};

When to use each:

  • Use Record<string, string> for clean, simple mappings with no additional properties.
  • Use { [key: string]: string } when you need flexibility or additional customization.

Config

The presence of a tsconfig.json file in a directory indicates that the directory is the root of a TypeScript project. The tsconfig.json file specifies the root files and the compiler options required to compile the project.

JavaScript projects can use a jsconfig.json file instead.

Immutability

Operators

Generic

keyof Type Operator

The keyof operator takes an object type and produces a string or numeric literal union of its keys.

The following type P is the same type as type P = "x" | "y":

type Point = { x: number; y: number };
type P = keyof Point;

Another example:

type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish;

//type A = number;

Indexed Access types

We can use an indexed access type to look up a specific property on another type:

type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"];

//type Age = number;

The indexing type is itself a type, so we can use unions, keyof, or other types entirely:

type I1 = Person["age" | "name"];

type I1 = string | number;

type I2 = Person[keyof Person];

type I2 = string | number | boolean;

type AliveOrName = "alive" | "name";
type I3 = Person[AliveOrName];

type I3 = string | boolean;

Another example of indexing with an arbitrary type is using number to get the type of an array’s elements. We can combine this with typeof to conveniently capture the element type of an array literal:

const MyArray = [
  { name: "Alice", age: 15 },
  { name: "Bob", age: 23 },
  { name: "Eve", age: 38 },
];

type Person = (typeof MyArray)[number];

// type Person = {
//   name: string;
//   age: number;
// };

Mapped types

A mapped type is a generic type which uses a union of PropertyKeys (frequently created via a keyof) to iterate through keys to create a type:

type OptionsFlags<Type> = {
  [Property in keyof Type]: boolean;
};

type Features = {
  darkMode: () => void;
  newUserProfile: () => void;
};

type FeatureOptions = OptionsFlags<Features>;

// type FeatureOptions = {
//     darkMode: boolean;
//     newUserProfile: boolean;
// }

Mapping modifiers

There are two additional modifiers which can be applied during mapping: readonly and ? which affect mutability and optionality respectively.

You can remove or add these modifiers by prefixing with - or +. If you don’t add a prefix, then + is assumed.

// Removes 'readonly' attributes from a type's properties
type CreateMutable<Type> = {
  -readonly [Property in keyof Type]: Type[Property];
};

type LockedAccount = {
  readonly id: string;
  readonly name: string;
};

type UnlockedAccount = CreateMutable<LockedAccount>;

type UnlockedAccount = {
  id: string;
  name: string;
};

Generic Constraints

Typescript cannot narrow down the property type in this case:

const testObj = { x: 10, y: "Hello", z: true };

function getProperty<T>(obj: T, key: keyof T) {
  return obj[key];
}

const xValue = getProperty(testObj, "x");
// const xValue: string | number | boolean

const yValue = getProperty(testObj, "y");
// const yValue: string | number | boolean

Use extends as generic Constraints

const testObj = { x: 10, y: "Hello", z: true };

function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

const xValue = getProperty(testObj, "x");
// const xValue: number
const yValue = getProperty(testObj, "y");
// const yValue: string

Conditional types

type IsString<T> = T extends string ? true : false;

Closures

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function’s scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.

A closure is a feature where a function retains access to its lexical scope, even when the function is executed outside of its original scope.

function outerFunction(outerVariable: string) {
  return function innerFunction(innerVariable: string) {
    console.log(`Outer Variable: ${outerVariable}`);
    console.log(`Inner Variable: ${innerVariable}`);
  };
}

const closureFunction = outerFunction("outside");
closureFunction("inside");
// Output:
// Outer Variable: outside
// Inner Variable: inside

Another example:

function createCounter() {
  let count = 0;
  return function () {
    count++;
    return count;
  };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2

Closures are a powerful and flexible feature, enabling patterns such as function factories, currying, and maintaining state.

Currying

Currying is a functional programming technique where a function that takes multiple arguments is transformed into a sequence of functions, each taking a single argument.

In essence, instead of calling a function with all its arguments at once, you call it with one argument at a time, returning a new function for each subsequent argument until all arguments are provided.

Currying is a technique that promotes modular and reusable code, especially in functional programming paradigms. It simplifies the handling of multi-argument functions and allows for elegant function composition and partial application.

Why Use Currying?

  • Reusability: It allows the creation of specialized functions by fixing some arguments.
  • Composition: Curried functions are easier to compose with other functions.
const multiply = (a: number) => (b: number) => a * b;

const triple = multiply(3);
console.log(triple(4)); // 12

Best Practices

Types

  • Don’t ever have a generic type which doesn’t use its type parameter.
  • Don’t use any as a type unless you are in the process of migrating a JavaScript project to TypeScript. In cases where you don’t know what type you want to accept, or when you want to accept anything because you will be blindly passing it through without interacting with it, you can use unknown
  • unknown is the type-safe counterpart of any. Anything is assignable to unknown, but unknown isn’t assignable to anything but itself and any without a type assertion or a control flow based narrowing. Likewise, no operations are permitted on an unknown without first asserting or narrowing to a more specific type.
  • Don’t use the return type any for callbacks whose value will be ignored. Use void as a return type
  • Use strict comparisons, we should make sure that we use ===`` and !==` for equality comparisons.
  • Use Strict String Expressions: foo ${bar} for string concatenation.
  • Add default for Switch
  • No Unnecessary Constructor. We shoudn’t have constructors that are redundant. JavaScript will add them for us without it.
  • Default Type Parameter. We can add a default type value to the generic type parameter.
    function foo<N = number, S = string>() {}
    
  • Explicitly writing void as the return type is optional, but it can be beneficial for clarity and when you want to explicitly state that a function does not return any meaningful value. Additionally, it helps prevent potential issues when using strict mode in TypeScript.
  • Don’t use optional parameters in callbacks unless you really mean it
  • It’s always legal for a callback to disregard a parameter, so there’s no need for the shorter overload. Here the done param can be discarded.
    /* OK */
    declare function beforeAll(
      action: (done: DoneFn) => void,
      timeout?: number
    ): void;
    
  • About Enum:

Function Overloads

Ordering

TypeScript chooses the first matching overload when resolving function calls. When an earlier overload is “more general” than a later one, the later one is effectively hidden and cannot be called.

  • Don’t put more general overloads before more specific overloads like this:

    /* WRONG */
    declare function fn(x: unknown): unknown;
    declare function fn(x: HTMLElement): number;
    declare function fn(x: HTMLDivElement): string;
    var myElem: HTMLDivElement;
    var x = fn(myElem); // x: unknown, wat?
    
  • Do sort overloads by putting the more general signatures after more specific signatures:

    declare function fn(x: HTMLDivElement): string;
    declare function fn(x: HTMLElement): number;
    declare function fn(x: unknown): unknown;
    var myElem: HTMLDivElement;
    var x = fn(myElem); // x: string, :)
    

Use Optional Parameters

Use optional parameters instead of several overloads:

interface Example {
  diff(one: string, two?: string, three?: boolean): number;
}

Use Union Types

You can replace the overloads:

/* WRONG */
interface Moment {
  utcOffset(): number;
  utcOffset(b: number): Moment;
  utcOffset(b: string): Moment;
}

with this:

/* OK */
interface Moment {
  utcOffset(): number;
  utcOffset(b: number | string): Moment;
}

Web

Tree Shaking

Tree shaking is a technique used by modern JavaScript bundlers, like Webpack or Rollup, to eliminate unused code (dead code) from the final bundle. The goal of tree shaking is to optimize the bundle size by removing any code that is not actually used in the application.

The term “tree shaking” comes from the idea of shaking a tree and letting the dead leaves fall off while keeping the healthy ones. In the context of JavaScript bundling, the “tree” refers to the dependency tree of the application, which represents the relationships between different modules and their dependencies.

This process is particularly beneficial for applications that rely on large libraries or frameworks, where not all features or modules are used.

Tree shaking in Angular

Register your service

Registering the provider in the @Injectable() metadata also allows Angular to optimize an app by removing the service from the compiled application if it isn’t used (tree-shaking).

Ensure your lib is tree-shakable

Minimize Side Effects: Avoid relying on global variables or causing unintended side effects during module initialization. Side effects can prevent the tree shaking process from removing unused code.

Pure Functions: Write pure functions whenever possible. Pure functions are more likely to be optimized and removed if they are not used.

Avoid Circular Dependencies: Circular dependencies can complicate the tree shaking process and may result in unused code not being properly eliminated. Keep your dependencies well-structured and avoid circular references.

Modular Code Organization: Organize your library code into small, focused modules. Each module should have a clear purpose and expose only the necessary APIs. This allows the tree shaking process to identify and remove unused code more effectively.

Export with caution

Adhere to the principle of encapsulation. Export Only What’s Needed: Export only the classes, components, services, and functions that are part of your library’s public API. Avoid exporting internal implementation details.

When internal details are exported, it can create complex interdependencies between different parts of the library. This complexity makes it harder for the tree shaker to accurately analyze and optimize the code, potentially resulting in unused code being retained.

The tree shaker relies on static analysis to determine which code paths are reachable and which are not. Exporting internal details can introduce dynamic or indirect dependencies that are difficult for the tree shaker to trace accurately.

Reduces Unused Code: When you export only the specific classes, components, services, and functions that are part of your library’s public API, the tree shaker can easily determine which parts of your library are being used and which are not. This allows it to strip away the unused code from the final bundle.

Optimizes Dependencies: By exporting only what’s needed, you prevent unnecessary dependencies from being included in the bundle. For example, if a component relies on a service, and that service is not used in the consuming application, the tree shaker can exclude both the component and the service from the bundle.

Simplifies Analysis: A smaller set of exports makes it easier for the tree shaker to analyze the relationships between different parts of your library. This streamlined analysis improves the accuracy of identifying and removing unused code.

@ViewChild and @ContentChild with caution

Using @ViewChild and @ContentChild decorators in an Angular library can potentially impact tree shaking if they are not used carefully.

Retaining Unused Components: If you use @ViewChild or @ContentChild to reference components, directives, or elements that are not used by the consuming application, the tree shaker may not be able to eliminate them from the bundle.

Complex Template Dependencies: When you use @ViewChild or @ContentChild, it creates dependencies between the parent component and the child component or element being referenced. This can make it more challenging for the tree shaker to accurately determine which parts of the code are actually used.

Indirect Dependencies: The dependencies introduced by @ViewChild or @ContentChild might not be straightforward or immediately obvious. This can lead to indirect dependencies being retained in the bundle, even if the referenced elements or components are not used.

Dynamic Template Changes: @ViewChild and @ContentChild bindings can sometimes involve dynamic template changes, which can make it harder for the tree shaker to perform static analysis and determine if a particular component or element is used.

Side effects

Side effects (such as direct dom manupulation, network requests) can negatively impact various aspects of application development and maintenance, including tree shaking, bundle size, performance, and behavior consistency.

Event Loop

  • JS runtime engine (like v8) contains the heap (where memory allocatin happens), and the stack (which includes the call stack).
  • WebApis are provided by browser, not by JS runtime
  • WebApis includes DOM, setTimeout
  • event loop, call back
  • JS is single threaded, a single callstack, it can do one thing at a time
  • a callstack is a data structure records where we are in the program, sort like the to-do list
  • we can’t block the stack because it’s in the browser and we want to have nice fluidUI, the solution is async callback
  • JS can have callback and other thing to run things async is because browser has other things like Web Apis
  • Any webapis (like callbacks in settimeout), when they’re done, they are pushed to the task queue
  • event loop’s job is to look at the stack and look at the task queue, if the stack is empty, it takes the first thing on the queue, and pushes it on to the stack.
  • if you want to do settimeout 0, is when you want to defer something until the stack is clear
  • the browser has a render queue, which gives a higher priority than the callback queue (microtask queue).
  • don’t block the event loop - don’t put slow code on the stack, it will block the render queue
  • Microtasks have a higher priority than macrotasks, meaning that they are executed before the event loop moves to the next macrotask. The microtask queue is processed completely before any macrotask is executed.
  • microtask vs macrotask

Memory Leak

Common memory leaks scenarios:

setTimeout and setInterval

When using setTimeout and setInterval, we need to clear them, otherwise this will cause memory leaks. Best practice is to provide a reference, and clear that reference on destroy. For example:

const timer=setInterval(
  doSomething();
, 1000); //provide a reference to the timer
clearInterval(timer); // clear the timer

console.log({object})

Retention of Object References: When console.log logs an object, it logs a reference to the object rather than its immediate value. This means: If the object is still in memory and logged to the console, the console holds a reference to it, preventing it from being garbage collected. This behavior is especially problematic if you’re logging large objects or objects that are updated frequently.

Deferred Evaluation in Developer Tools: When you log an object or array, the actual evaluation and display of the logged value are deferred until you expand the log in the console. By that time, the object might have already changed due to subsequent operations in the code.

For example:

const largeObject = { data: new Array(1000000).fill("large") };
console.log(largeObject); // Large object retained in memory

Webpack

Webpack is a JavaScript module bundler. It is used in web development to bundle and package assets. Assets such as JS files, CSS files, images and such. It bundles these assets into a single optmized bundle that can be served in the web browser.

The main purpose of Webpack is to take a dependency graph of modules (files) and generate a single bundle that contains all the necessary code for the application to run. This process includes resolving dependencies between modules, handling different types of assets, and applying various optimizations to reduce the size and improve the performance of the final bundle.

It simplifies the management of dependencies and improves the performance of web applications by reducing file sizes and enabling advanced features like code splitting and lazy loading.

Features

  • Entry Point: Webpack starts bundling from one or more entry points, which are the entry files of your application. It analyzes these entry points and builds a dependency graph by following import statements.
  • It’s easier to handle and faster to serve because everything is neatly packed together.
  • Webpack is smart too! It knows when to break the big cookie into smaller ones (code splitting) so that you can serve only what your visitors need at a particular moment. This makes your website faster and saves bandwidth.
  • Code Splitting: Webpack allows you to split your code into multiple chunks, which can be loaded asynchronously, improving the initial loading time of your application.

Reduce bundle size

Ways to reduce bundle size:

  • Use Angular CLI Builders: The Angular CLI has built-in builders that can help optimize your bundles, such as the @angular-devkit/build-optimizer. This can further reduce the size of your code.
  • Lazy Load Third-Party Libraries: If you’re using third-party libraries, load them lazily only when needed, rather than including them in the main bundle.
  • Optimize CSS: Minify and optimize your CSS. You can use tools like PostCSS to remove unused styles and reduce the size of your stylesheets.
  • Code splitting: lazy-load modules. This means that a module and its dependencies will only be loaded when the user navigates to a route that uses that module.
  • Tree shaking: use a bundler like Webpack that supports tree shaking.
  • Avoid using wildcard imports: Wildcard imports, such as import * as myModule from ‘./my-module’, can prevent tree shaking from working effectively. Instead of using wildcard imports, consider using named imports, such as import { myFunction } from ‘./my-module’.
  • Use tools like webpack-bundle-analyzer to analyze your bundle and identify large chunks that could be optimized.

Federation

In web development, federation refers to the practice of distributing and sharing responsibilities across multiple independent systems or services. The concept of federation is particularly common in the context of microservices architecture, where different services work together to build a larger, more complex application.

In a federated system, each service is autonomous and responsible for a specific set of functionalities. These services can communicate with each other through well-defined interfaces, enabling them to collaborate and provide a cohesive experience to users. The main idea behind federation is to promote loose coupling and scalability, as individual services can be developed, deployed, and maintained independently.

Module Federation

Module Federation is a feature provided by Webpack, the popular JavaScript module bundler. It allows multiple independent frontend applications to dynamically load and use code from each other at runtime. In simpler terms, it enables sharing of JavaScript modules (components, libraries, etc.) across different frontend applications.

Module Federation provides a solution to the scaling problem by allowing a Single Page Application (SPA) to be sliced into multiple smaller remote applications that are built independently.

With Module Federation, a large application is split into:

  1. A single Host application that references external…
  2. Remote applications, which handle a single domain or feature.

Downsides:

  • Developers need to think about which remotes they are working on, since it is a waste of CPU and memory to run all remotes in development mode. In practice this may not be a problem if the teams are already divided by domain or feature.
  • Increased orchestration since remotes are independent of each other, shared state may require the host application to coordinate it between remotes. For example, sharing Redux state across remotes is more complicated versus a SPA.
  • Version-Mismatch-Hell where different applications are deployed with different versions of shared libraries can lead to unexpected errors.

Module Federation vs Micro Frontend

While Module Federation enables faster builds by vertically slicing your application into smaller ones, the MFE architecture layers independent deployments on top of federation. The Micro Frontend (MFE) architecture builds on top of Module Federation by providing independent deployability. Teams should only choose MFEs if they want to deploy their host and remotes independently.

Native Federation

Native Federation is a browser-native implementation that can be used independently of build tools and frameworks. Browser-native means that we rely on modern and future browser technologies such as EcmaScript Modules and Import Maps.

Atomic

Concept

In programming, “atomic” refers to an operation or a set of operations that are executed as a single, indivisible unit. The term is commonly used in the context of concurrent programming, where multiple threads or processes may access shared resources or data simultaneously.

Goal

The primary goal of atomic operations is to ensure that certain critical sections of code are executed without interruption or interference from other threads or processes. This prevents potential race conditions, data corruption, or other undesired behavior that could arise from concurrent access to shared resources.

For example, consider a scenario where two threads need to increment a shared variable simultaneously. If the increment operation is not atomic, it is possible that both threads could read the current value of the variable, increment it separately, and then write back their updated values. As a result, one of the increments would be lost, and the final value of the variable would be incorrect.

By making the increment operation atomic, both threads are guaranteed to read the current value, perform the increment, and write back the updated value as a single, uninterrupted operation. This ensures that the variable’s value is correctly incremented, regardless of how many threads access it simultaneously.

Methods

Atomicity can be achieved using various mechanisms provided by programming languages and platforms, such as atomic instructions, locks, mutexes, semaphores, or compare-and-swap operations.

Build once, deploy everywhere

“Build once, deploy everywhere” is the concept of being able to create a single build artefact of your application and deploy it to multiple environments such as staging and production.

Dynamic Module Federation is a technique that allows an application to determine the location of its remote applications at runtime. It helps to achieve the use case of “Build once, deploy everywhere”.

The difficulty in achieving this with a Micro Frontend Architecture using Static Module Federation is that our Remote applications will have a different location (or URL) in each environment. Previously, to account for this, we would have had to specify the deployed location of the Remote applications and rebuild the application for the target environment.

Using Dynamic Module Federation, you can easily achieve this.

Security

Certificates

Client Certificates

A client certificate and key are components used in SSL/TLS (Secure Sockets Layer/Transport Layer Security) protocols to establish secure communication between a client (such as a web browser) and a server. While server certificates are used to authenticate the server to the client, client certificates are used to authenticate the client to the server.

Client certificates are commonly used in scenarios where the server needs to verify the identity of the connecting client. This is more common in enterprise environments, VPNs (Virtual Private Networks), and certain web applications that require strong client authentication. The use of client certificates adds an extra layer of security by ensuring that both the server and the client can authenticate each other in a mutually trusted manner.

Server Certificates

The server certificate serves as a way to verify the authenticity of the server to the client. It includes information about the server, such as its domain name, the name of the organization that owns the server, and the digital signature of the certificate authority (CA) that issued the certificate.

One of the primary purposes of SSL/TLS is to encrypt the communication between the client and the server. The server certificate contains a public key, which is used for encryption. When a client connects to a secure website (using HTTPS), the client and server negotiate a shared secret key for encrypting and decrypting data during the session.

Use of both

When a client connects to a server, the server certificate is always used to establish the server’s identity. Whether a client certificate is used depends on the server’s configuration and security requirements. If client authentication is required, the client certificate and associated private key are used during the SSL/TLS handshake process to provide proof of the client’s identity to the server.

OAuth

OpenID Connect (OIDC)

OpenID Connect (OIDC) is an open authentication protocol that works on top of the OAuth 2.0 framework. Targeted toward consumers, OIDC allows individuals to use single sign-on (SSO) to access relying party sites using OpenID Providers (OPs), such as an email provider or social network, to authenticate their identities.

OAuth 2.0

OAuth 2.0 is the industry-standard protocol for authorization. OAuth 2.0 focuses on client developer simplicity while providing specific authorization flows for web applications, desktop applications, mobile phones, and living room devices.

NPM package angular-oauth2-oidc provides support for OAuth 2 and OIDC in Angular.

ID Token vs. Access Token

  • ID Token (OIDC): Contains claims about the authenticated user (e.g., sub, email, name).
  • Access Token (OAuth2): Used for authorizing API access, but does not inherently contain user identity details.
    FeatureOAuth 2.0OIDC
    PurposeAuthorizationAuthentication + Authorization
    TokensAccess Token, Refresh TokenID Token, Access Token, Refresh Token
    User Authentication❌ No✅ Yes
    Used for API access✅ Yes✅ Yes
    Used for Login (SSO, Federated Identity)❌ No✅ Yes
    Discovery Endpoint❌ No✅ Yes

Authorization Code Workflow with PKCE

The Authorization Code Flow is the most secure way to authenticate users and obtain access tokens in OAuth 2.0 and OpenID Connect (OIDC). It involves exchanging an authorization code for an access token (and optionally an ID token in OIDC).

  1. User initiates login, sends state, nonce and code challenge -> Redirected to Authorization Server.
  2. User logs in and grants permission. The user authenticates on the Authorization Server. The state is included in the redirect response. If authentication is successful, the user is asked to approve or deny the authorization request (this includes verifying the state parameter matches the request, if not, reject the response to prevent CSRF attacks).
  3. If the user approves, the authorization server redirects the user back to the application with an authorization code.
  4. Client exchanges authorization code for an access token (and an ID token in OIDC). The ID token contains user identity information.
  5. When the client request the access token, in addition to the authorization code, it also includes the original code verifier used to generate the code challenge. The authorization server verifies the code challenge in the client request.
  6. The client decodes the ID token and verifies: Does the nonce inside the ID token match the nonce originally sent? If not, reject the ID token because it may be a replay attack.
  7. Client uses access token to access APIs.
  8. (Optional) Client refreshes token when it expires.

Authorization Code Flow with PKCE

PKCE (Proof Key for Code Exchange, pronounced “pixy”) is an OAuth 2.0 security extension that prevents authorization code interception attacks. It is mandatory for public clients like SPAs and mobile apps that cannot safely store a client secret.

Why is PKCE Needed? In a traditional Authorization Code Flow (without PKCE), an attacker could intercept the authorization code and use it to obtain an access token. Since public clients (e.g., SPAs, mobile apps) don’t have a client secret, there’s no way to verify that the request for an access token came from the legitimate client.

PKCE solves this problem by adding a randomly generated code challenge that binds the authorization request to the token request.

How PKCE Works (Step-by-Step)

PKCE modifies the Authorization Code Flow by adding two extra values:

  • Code Verifier (random secret stored in the client)
  • Code Challenge (derived from the code verifier and sent to the authorization server)

1. SPA/Mobile App Generates a Code Verifier & Code Challenge

  • A random string (code_verifier) is generated (43-128 characters).
  • A hashed version (code_challenge) is derived using SHA-256.

2. Client Requests Authorization Code

  • The SPA redirects the user to the authorization server with:
  • client_id
  • redirect_uri
  • response_type=code
  • code_challenge (the hashed value)
  • code_challenge_method=S256

3. User Logs In & Gets an Authorization Code

  • The authorization server authenticates the user.
  • It stores the code_challenge and sends the authorization code back to the SPA.

4. Client Exchanges Code for an Access Token

  • The SPA sends a backend request to exchange the code for an access token.
  • The request includes the original code_verifier.

5. Authorization Server Verifies the Code Verifier

  • The server recomputes the code_challenge from the code_verifier and compares it with the stored code_challenge.
  • If they match, the server issues an access token.

ROPC

The Resource Owner Password Credentials (ROPC) Grant in OAuth2 allows a user to authenticate by directly sending their username and password to the application, which then exchanges them for an access token. While this method may seem simple, it is generally not recommended for modern applications due to several security risks and limitations:

  • Security Risk: The client application directly handles user credentials, increasing the risk of credential leaks.
  • No MFA Support: It does not support modern authentication methods like multi-factor authentication (MFA) or Single Sign-On (SSO).
  • Requires High Trust in Clients: The client must be fully trusted since it handles passwords directly.

Only recommend this when other options are not viable.

Implicit flow

Not recommended due to security risks. Implicit flow (directly returns access token), Previously used by SPAs to directly receive an access token in the URL, but is vulnerable to token leakage. Implicit flow does not involve a code, after user authenticates on the authorization server, it immediately returns an access token in the Url.

Keycloak

In Keycloak, since the login page is served from Keycloak’s backend, credentials never touch the client application directly.

Is the Access Token Exposed to the Browser?

If an application stores the access token in session storage, JavaScript code in the same origin can access it. This means if an attacker injects malicious JavaScript via Cross-Site Scripting (XSS), they can steal the token.

However, the token is not directly exposed in the URL (unlike in Implicit Flow), reducing exposure risk.

State and Nounce

Both the state and nonce parameters are security mechanisms used in OAuth 2.0 (and more specifically in OpenID Connect) to help protect the integrity of the authentication flow. Although they are similar in that they involve passing a random value back and forth between the client and the authorization server, they serve distinct purposes and are used at different stages of the flow.

State

  • Generation: When initiating the OAuth2 authorization request, the client application generates a unique and unpredictable string. This string might also include additional encoded data (like the URL the user was trying to access).
  • Inclusion in Request: The generated state is sent as a query parameter (e.g., ?state=abc123…) along with the other OAuth parameters.
  • Round-Trip: The authorization server receives the request and then includes the same state parameter in its response (either in the query string or fragment of the redirect URI).
  • Verification: When the client receives the response, it checks that the state value matches the one it originally sent. A mismatch indicates that the response might have been tampered with or initiated from an unauthorized source, prompting the client to reject the authentication response.

CSRF vs Replay Attack vs XSS

Cross-Site Request Forgery (CSRF) is when an attacker tricks a user’s browser into making an unwanted request to a website where the user is authenticated. The attacker might craft a malicious link or form that, when clicked or auto-submitted by the user’s browser, sends a request (like a fund transfer or data change) to the target site. Since the browser automatically sends cookies and authentication tokens, the site treats the request as legitimate.

Replay attack: A replay attack occurs when an attacker intercepts valid data (like an authentication token or transaction message) and later reuses that data to repeat a request or transaction. Prevention methods include using one-time tokens (nonces), timestamps, and session identifiers that are valid only for a short period or a single use. This is why OAuth flows use a nonce—to bind the token to a specific request and prevent its reuse.

Cross-Site Scripting (XSS): XSS is a vulnerability where an attacker injects malicious scripts into web pages viewed by other users. The attacker exploits weaknesses in input validation to insert JavaScript (or other code) into a page. When other users view the page, the malicious code runs in their browsers, potentially stealing data, manipulating the page, or performing actions on behalf of the user. Defenses include proper output encoding, input validation, using Content Security Policy (CSP) headers.

CSRF: Targets the user’s authenticated session by tricking their browser into submitting unintended requests. It leverages the fact that browsers automatically send credentials (like cookies) with each request.

Replay Attack: Involves capturing and reusing valid data (such as a token or a signed message) to perform unauthorized actions. It relies on the reuse of a message rather than tricking the browser.

XSS: Occurs when an attacker injects malicious code into a web page, which then executes in the browser of anyone who views the page. Unlike CSRF and replay attacks, XSS directly affects the client-side code by executing scripts that the attacker controls.

How State and Nounce are used to prevent these attacks

State Prevents CSRF:

When the client initiates an OAuth/OIDC authentication request, it generates a unique state value and sends it along with the request. Once the authorization server responds, the client checks that the returned state matches the original value. This check confirms that the response was triggered by the client’s own request and not by a malicious third party (which might try to trick the user’s browser into sending a forged request). In this way, the state parameter helps prevent Cross-Site Request Forgery (CSRF) attacks.

In summary, the state parameter ensures that the response is associated with the proper request, the authorization request and response belong to the same client (blocking CSRF), and the nonce confirms that the token is unique to the current session (blocking replay attacks). Neither state nor nonce protects against vulnerabilities like XSS; they are specifically designed to secure the OAuth/OIDC authentication process.

Featurestatenonce
PurposePrevents CSRF attacksPrevents Replay attacks
Where is it used?Sent in login request & redirect URLSent in login request & JWT (ID Token)
What does it protect?Ensures the redirect response is validEnsures the token is not reused
Checked against?The redirect URL from the Identity ProviderThe JWT (ID Token) received after login
Generated by?Your Angular appYour Angular app
Checked by?Your Angular app when processing the redirectYour Angular app before using the ID Token
Used inOAuth 2.0OpenID Connect (OIDC)

Self-signed Certificate

A website’s certificate is trusted by default if it’s signed by a well-known CA. However, for local dev and testing of https, a self-signed certificate is often needed. Below steps show how to create a custom CA and create a self-signed certificate with it.

Create a root CA certificate

  1. Create a root key:
    openssl ecparam -out customca.key -name prime256v1 -genkey
    
  2. Create a root signing request:
    openssl req -new -sha256 -key customca.key -out customca.csr
    
  3. Self-sign:
    openssl x509 -req -sha256 -days 365 -in customca.csr -signkey customca.key -out customca.crt
    

This creates the root CA certificate customca.crt. You will use this to sign the server certificate.

Create a server certificate

  1. Create a server key:
    openssl genrsa -out server.key 2048
    
  2. Create a request configuration file to to handle SAN. Important: localhost must be in either the CN (Common Name) or the list of subjectAltName. Also note the CN for the server certificate must be different from the issuer’s domain. For example, the CN for the issuer is www.customcaissuer.com and the server certificate’s CN is www.localhost.com.
    [req]
    distinguished_name = req_distinguished_name
    x509_extensions = v3_req
    prompt = no
    [req_distinguished_name]
    C = US
    ST = DC
    L = DC
    O = OR
    OU = OU
    CN = www.localhost.com
    [v3_req]
    keyUsage = critical, digitalSignature, keyAgreement, keyCertSign
    extendedKeyUsage = serverAuth
    subjectAltName = @alt_names
    [alt_names]
    DNS.1 = localhost
    DNS.2 = 127.0.0.1
    DNS.3 = www.localhost.com
    DNS.4 = localhost.com
    
  3. Create a certificate signing request:
    openssl req -new -sha256 -key server.key -out server.csr -config req.cnf
    
  4. Self-sign using custom CA. Important: this command should include an extension which also uses the req.cnf for the subjectAltName configuration.
    openssl x509 -req -in server.csr -CA  mwsca.crt -CAkey mwsca.key -CAcreateserial -out server.crt -days 999 -sha256 -extensions v3_req -extfile req.cnf
    

Add the root certificate to your machine’s trusted root store

Add the root CA certificate to your machine’s trusted root store. When you access the website, ensure the entire certificate chain is seen in the browser.

However, if for security reason, the machine’s trusted root store cannnot be modified, you can add the root CA certificate for each client trying to access the server. For example, if accessing using a web browser, add the root CA certificate to the browser trusted root. If using Postman, add the root CA certificate as the trusted CA within Postman.

CORS

CORS stands for Cross-Origin Resource Sharing.

Imagine you’re on a website, let’s call it “Website A”. In your browser, when Website A tries to make a request (like loading an image, fetching data, or submitting a form) to another website, let’s call it “Website B,” the browser might stop that request. This security feature is called the “Same-Origin Policy.”

Now, CORS comes into play when you actually want Website A to be able to make requests to Website B. CORS is like a set of rules that allows or restricts these kinds of requests between different websites.

Breakdown:

  • Same-Origin Policy: By default, browsers don’t allow a web page to make requests to a different domain than the one that served the web page. This is to prevent potentially malicious actions.
  • CORS to the Rescue: Sometimes, you want Website A to be able to make requests to Website B. CORS is a way for Website B to say, “Hey, Website A, you’re allowed to use my stuff.” It does this by adding some extra headers to its responses.
  • The CORS Headers: When Website A makes a request to Website B, Website B can include special CORS headers in its response. These headers tell the browser, “It’s okay, Website A, you can use my resources.” Without these headers, the browser might block the request.
  • CORS Middleware in Express: In the context of Express, you can use a middleware called cors to easily add these CORS headers to your responses. It’s like a helper that takes care of the CORS-related stuff for you.

So, in summary, CORS is a set of rules and headers that allows one website to ask for permission from another website to use its resources. It’s like a friendly conversation between websites that helps ensure security on the web while allowing for necessary interactions between different domains.

Workflow

  1. Client (Browser) Sends Request to Server: Website A (client) sends an HTTP request to Website B (server).
  2. Browser Blocks Request: The SOP kicks in, and by default, the browser blocks the request from Website A to Website B because they are different origins.
  3. CORS Headers in Server’s Response: Despite the initial request being blocked, if Website B is configured to support CORS, it includes specific CORS headers in its response. These headers, such as Access-Control-Allow-Origin, Access-Control-Allow-Methods, and others, declare which origins, methods, and headers are allowed to access its resources.
  4. Browser Processes Headers: The browser, upon receiving the response with CORS headers, checks if Website A is permitted by the server. If the server allows it, the browser allows the response to be processed, and the requested data/resource may be accessed by the client-side JavaScript code on Website A.

So, it’s the server’s responsibility to include the appropriate CORS headers in its response to inform the browser about which origins are allowed to access its resources, even though the browser initially blocks the request based on the SOP. This way, CORS provides a controlled way for servers to selectively enable cross-origin requests.

Dynamic vs Static Typed

Dynamically Typed Languages

In dynamically typed languages, the type of a variable is determined at runtime, allowing more flexibility but potentially leading to runtime errors.

Characteristics:

  • Types are inferred automatically.
  • Errors related to types are caught at runtime.
  • Great for rapid prototyping and scripting tasks.

Statically Typed Languages

In statically typed languages, the type of a variable is determined at compile-time, providing better performance and error detection before execution.

Type SystemCharacteristicsExamples
Dynamically TypedRuntime type checking, flexible but riskierPython, JavaScript, Ruby
Statically TypedCompile-time type checking, safer but stricterJava, C, Rust, Go
MixedAllows both static and dynamic typingTypeScript, Dart, Python (with hints)

API Design

GraphQL

GraphQL Error Design

Two types of approaches:

References

Two good articles on best practices on GraphQL error design:

Best Practices

  • The error field: The error field has no schema. A potential issue with the error field is that when they are present, the corresponding field should be null. This can potentially be a deal breaker if you’re wanting to have errors returned as part of a mutation, but want to query for data on the result anyways. A common example of this is the server sending back the actual state of a resource after a mutation that had errors. With top-level errors this can’t be done since the whole mutation field should be null!.
  • GraphQL errors encode exceptional scenarios: like a service being down, unauthentication, rate limit, timeout, syntax, or some other internal failure.
  • Errors which are part of the API domain should be captured within that domain.
  • User facing or actionable errors should be returned as data.

In summary:

  • Critical errors that cannot be fixed by clients (e.g. a database error) - returned in error field
  • Recoverable errors that can be fixed by clients (e.g. invalid input data) - returned as data

Use Union Result Types

Example:

#![allow(unused)]
fn main() {
type Mutation {
  register(email: String!, password: String!): RegisterResult!
}

// Use different types for all possible actionable errors
union RegisterResult = User | ValidationError | ProfessionalEmailRequired

type User {
  id: ID!
  email: String!
}

// Malformed inputs
type ValidationError {
  // Allow several errors at the same time!
  fieldErrors: [FieldError!]!
}

type FieldError {
  path: String! // Shows where the errors are to be displayed in UI
  message: String!
}

// Business specific errors (e.g. banned email providers)
type ProfessionalEmailRequired {
  provider: String!
}
}

Object Oriented Programming

OOP Features

From the Gang of Four book: Object-oriented programs are made up of objects. An object packages both data and the procedures that operate on that data. The procedures are typically called methods or operations.

Encapsulation that Hides Implementation Details

The only way to interact with an object is through its public API. The implementation details of an object are not accessible to code using that object. For example, in rust we can define:

#![allow(unused)]
fn main() {
pub struct AveragedCollection {
    list: Vec<i32>, //private
    average: f64, //private
}
}
#![allow(unused)]
fn main() {
impl AveragedCollection {
    pub fn add(&mut self, value: i32) {
        self.list.push(value);
        self.update_average();
    }

    pub fn remove(&mut self) -> Option<i32> {
        let result = self.list.pop();
        match result {
            Some(value) => {
                self.update_average();
                Some(value)
            }
            None => None,
        }
    }

    pub fn average(&self) -> f64 {
        self.average
    }

    fn update_average(&mut self) {
        let total: i32 = self.list.iter().sum();
        self.average = total as f64 / self.list.len() as f64;
    }
}
}

We can change the data structure from a vector to a hashmap without breaking the public API.

Inheritance

Two reasons to use inheritance:

  1. Reuse of code
  2. Polymorphism: make use of the type system. To enable a child type to be used in the same places as the parent type. Polymorphism means you can substitute multiple objects for each other at runtime if they share certain characteristics.

Inheritance has recently fallen out of favor as a programming design solution in many programming languages because it’s often at risk of sharing more code than necessary. It is less flexible that a subclass share all characteristics of their parent class.

State Pattern

The State Pattern is a behavioral design pattern used in object-oriented programming. It allows an object to change its behavior when its internal state changes, making the object appear to change its class. It is generally used when an object must behave differently based on its internal state.

Structure

The pattern typically includes:

  • Context – the main object that holds a reference to a state object.
  • State Interface – defines the behavior expected of all states.
  • Concrete States – implement specific behaviors and can change the context’s current state.

Example in TypeScript

// State interface
interface State {
  handle(context: Context): void;
}

// Concrete States
class StateA implements State {
  handle(context: Context): void {
    console.log("Handling in State A");
    context.setState(new StateB());
  }
}

class StateB implements State {
  handle(context: Context): void {
    console.log("Handling in State B");
    context.setState(new StateA());
  }
}

// Context
class Context {
  private state: State;

  constructor(state: State) {
    this.state = state;
  }

  setState(state: State): void {
    this.state = state;
  }

  request(): void {
    this.state.handle(this);
  }
}

// Usage
const context = new Context(new StateA());
context.request(); // Handling in State A
context.request(); // Handling in State B

Pros

  • Makes state-specific behavior easier to manage and extend.
  • Encourages encapsulation and separation of concerns.

Cons

  • Increases the number of classes
  • Can be an overkill if only a few states exists