NgStore Class Abstract

The NgStore class is a foundational component for implementing reactive state management and handling asynchronous data flow in modules or entire applications. It serves as a central hub, organizing and managing store items like emitters, states, and groups, ensuring seamless interaction among them.

Stores can also include methods to trigger specific actions, making them a powerful and flexible tool for coordinating complex application logic. Their structured design simplifies the development of scalable, maintainable, and reactive applications, ensuring consistency and clarity in managing state and data flow.

By implementing the StoreHooks interface, the store provides lifecycle hooks for executing custom logic before and after key events, such as store initialization and completion.

The NgStore class extends Store and serves as an abstract foundation designed to serve as a base for specific store implementations in Angular applications that define concrete collections of store items

API

abstract class NgStore extends Store {
  initialize(beforeInit?: (store: this) => void): this;
  complete(): void;
  unregisterOnDestroy(): this;
  protected markAsReady(): void;
}

Example

products.store.ts
import {computed, Injectable} from '@angular/core';
import {switchMap} from 'rxjs';
import {emitter, transmit} from '@bitfiber/rx';
import {NgStore, asyncSignalGroup, routeFiltersGroup, signalState} from '@bitfiber/ng/rx';
 
interface Product {
  id: number;
  name: string;
  price: number;
}
 
interface ProductCategory {
  id: number;
  name: string;
}
 
interface ProductsFilters {
  search: string;
  page: number;
}
 
interface ProductsState {
  categories: ProductCategory[];
  products: Product[];
}
 
@Injectable()
class ProductsStore extends NgStore {
  // Provides the start of the first data loading process
  start = emitter<void>();
 
  // Provides the filters state that is synchronized with the route and the filters form
  routeFilters = routeFiltersGroup<ProductsFilters>({
    initialQueryParams: {search: '', page: 1},
  }, ({filters}) => {
    filters.useLazyEmission();
  })
 
  // Provides an async group for managing categories loading process
  categoriesReq = asyncSignalGroup<void, ProductCategory[], Error>((categoriesReq, {launch}) => {
    launch
      // Triggers categories loading once the start emitter emits
      .wait(this.start)
      // Defines a side effect for loading categories
      .effect(switchMap(() => categoriesService.get().pipe(transmit(categoriesReq))));
  }, []);
  
  // Provides an async group for managing products loading process
  productsReq = asyncSignalGroup<ProductsFilters, Product[], Error>((productsReq, {launch}) => {
    launch
      // Triggers products loading after categories are successfully loaded
      .wait(this.categoriesReq.success, () => this.routeFilters.filters())
      // Reloads products when filters are updated
      .receive(this.routeFilters.filters)
      // Defines a side effect for loading products
      .effect(switchMap(filters => productsService.get(filters).pipe(transmit(productsReq))));
  }, []);
  
  // Provides the loading status state
  isLoading = computed(() => this.categoriesReq.state().inProgress
    || this.productsReq.state().inProgress);
 
  // Provides the main store state
  data = signalState<ProductsState>({categories: [], products: []}, s => s
    // Combines data from categories and products into a single state
    .select(
      this.categoriesReq.success,
      this.productsReq.success,
      (categories, products) => ({categories, products}),
    ),
  );
 
  // Provides the store error handling
  errors = emitter<Error>(e => e
    // Collects errors from asynchronous actions
    .receive(this.categoriesReq.fail, this.productsReq.fail)
    // Handle errors with a side effect
    .tap(error => console.log('Error:', error)));
 
  // Marks the store as ready, indicating that all store items have been defined
  #ready = this.markAsReady();
  
  // Automatically starts the store after initialization
  afterStoreInit(): void {
    this.start.emit();
  }
}
products.component.ts
import {Component, inject} from '@angular/core';
import {ReactiveFormsModule} from '@angular/forms';
 
@Component({
  selector: 'bf-products',
  standalone: true,
  templateUrl: './products.component.html',
  imports: [ReactiveFormsModule],
  providers: [ProductsStore],
})
export class ProductsComponent {
  readonly store = inject(ProductsStore).initialize();
  readonly form = this.store.routeFilters.form;
}
products.component.html
@if (!store.isLoading()) {
  <div
    class="bf-filters"
    [formGroup]="form"
  >
    <input formControlName="search"/>
    <pagenator formControlName="page"/>
  </div>
 
  @for (product of store.data().products; track product.id) {
    <div class="bf-product">
      {{product.name}} - {{product.price}}
    </div>
  }
} @else {
  Data is loading...
}