NgStoreNgStore Intro

What is NgStore?

The NgStore is an Angular-specific extension of the Basic Store and serves as a foundational component for implementing reactive state management and handling asynchronous data flow in Angular applications. It serves as a central hub, organizing and managing store items like emitters, states, and groups, ensuring seamless interaction among them.

ℹ️

For more detailed information about using stores, refer to the Basic Store Documentation.

Creating a NgStore

To create a store, define a new class that extends the NgStore class.

⚠️

The store must be an Angular service or component because it relies on Angular’s DestroyRef for managing the store’s completion.

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...
}