StoreStore Intro

What is Store?

The Store 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.

Creating a Store

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

import {switchMap} from 'rxjs';
import {Store, asyncGroup, emitter, state, transmit} from '@bitfiber/rx';
 
// Define a products store
class ProductsStore extends Store {
  // Define your store items here
}

Adding Store Items

Within the store class, define the store items you need, such as emitters, states, or groups. Each item will serve a specific purpose in managing the store’s reactive logic and state.

class ProductsStore extends Store {
  // Provides the start of the first data loading process
  start = emitter<void>();
 
  // Provides the state of the products filters
  filters = state<ProductsFilters>({search: '', page: 1});
 
  // Provides an async group for managing categories loading process
  categoriesReq = asyncGroup<void, ProductCategory[], Error>();
 
  // Provides an async group for managing products loading process
  productsReq = asyncGroup<ProductsFilters, Product[], Error>();
  
  // Provides the loading status state
  isLoading = state<boolean>(false);
 
  // Provides the main store state
  data = state<ProductsState>({categories: [], products: []});
 
  // Provides the store error handling
  errors = emitter<Error>();
}

Adding Interaction Logic

Use onInit callbacks within emitters, states, and groups to define side effects, establish relationships between store items, and manage their interactions effectively. This ensures subscriptions and emissions occur only after the store is initialized.

class ProductsStore extends Store {
  start = emitter<void>();
  
  filters = state<ProductsFilters>({search: '', page: 1})
    .useLazyEmission();
  
  categoriesReq = asyncGroup<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))));
  }, []);
  
  productsReq = asyncGroup<ProductsFilters, Product[], Error>((productsReq, {launch}) => {
    launch
      // Triggers products loading after categories are successfully loaded
      .wait(this.categoriesReq.success, () => this.filters())
      // Reloads products when filters are updated
      .receive(this.filters)
      // Defines a side effect for loading products
      .effect(switchMap(filters => productsService.get(filters).pipe(transmit(productsReq))));
  }, []);
  
  isLoading = state<boolean>(false, s => s
    // Tracks loading status based on the state of asynchronous actions
    .select(
      this.categoriesReq.state,
      this.productsReq.state,
      (categoriesState, productsState) => categoriesState.inProgress || productsState.inProgress,
    ),
  );
  
  data = state<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}),
    ),
  );
  
  errors = emitter<Error>(e => e
    // Collects errors from asynchronous actions
    .receive(this.categoriesReq.fail, this.productsReq.fail)
    // Handles errors with a side effect
    .tap(error => console.log('Error:', error)));
}

Marking Store as Ready

To finalize the store setup and indicate that all store items have been defined, call the markAsReady method.

class ProductsStore extends Store {
  start = emitter<void>();
  filters = state<ProductsFilters>({search: '', page: 1});
  categoriesReq = asyncGroup<void, ProductCategory[], Error>();
  productsReq = asyncGroup<ProductsFilters, Product[], Error>();
  isLoading = state<boolean>(false);
  data = state<ProductsState>({categories: [], products: []});
  errors = emitter<Error>();
 
  // Marks the store as ready, indicating that all store items have been defined
  #ready = this.markAsReady();
}

Lifecycle Hooks

You can use lifecycle hooks like beforeStoreInit, afterStoreInit, beforeStoreComplete, and afterStoreComplete to add custom logic at key lifecycle events.

class ProductsStore extends Store {
  start = emitter<void>();
  filters = state<ProductsFilters>({search: '', page: 1});
  categoriesReq = asyncGroup<void, ProductCategory[], Error>();
  productsReq = asyncGroup<ProductsFilters, Product[], Error>();
  isLoading = state<boolean>(false);
  data = state<ProductsState>({categories: [], products: []});
  errors = emitter<Error>();
  
  // Lifecycle hook: Automatically starts the store after initialization
  afterStoreInit(): void {
    this.start.emit();
  }
}

Action Methods

Define action methods to trigger specific store logic.

class ProductsStore extends Store {
  start = emitter<void>();
  filters = state<ProductsFilters>({search: '', page: 1});
  categoriesReq = asyncGroup<void, ProductCategory[], Error>();
  productsReq = asyncGroup<ProductsFilters, Product[], Error>();
  isLoading = state<boolean>(false);
  data = state<ProductsState>({categories: [], products: []});
  errors = emitter<Error>();
  
  // Action method: Changes the filters applied to the products
  updateFilters(filters: Partial<ProductsFilters>): void {
    this.filters.update(state => ({...state, ...filters}));
  }
}

Full Store Example

Below is a complete example of a reactive store for managing products, categories, and filters.

import {switchMap} from 'rxjs';
import {Store, asyncGroup, emitter, state, transmit} from '@bitfiber/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[];
}
 
class ProductsStore extends Store {
  // Provides the start of the first data loading process
  start = emitter<void>();
 
  // Provides the state of the products filters
  filters = state<ProductsFilters>({search: '', page: 1})
    .useLazyEmission();
  
  // Provides an async group for managing categories loading process
  categoriesReq = asyncGroup<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 = asyncGroup<ProductsFilters, Product[], Error>((productsReq, {launch}) => {
    launch
      // Triggers products loading after categories are successfully loaded
      .wait(this.categoriesReq.success, () => this.filters())
      // Reloads products when filters are updated
      .receive(this.filters)
      // Defines a side effect for loading products
      .effect(switchMap(filters => productsService.get(filters).pipe(transmit(productsReq))));
  }, []);
  
  // Provides the loading status state
  isLoading = state<boolean>(false, s => s
    // Tracks loading status based on the state of asynchronous actions
    .select(
      this.categoriesReq.state,
      this.productsReq.state,
      (categoriesState, productsState) => categoriesState.inProgress || productsState.inProgress,
    ),
  );
 
  // Provides the main store state
  data = state<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)
    // Handles 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();
  }
  
  // Changes the filters applied to the products
  updateFilters(filters: Partial<ProductsFilters>): void {
    this.filters.update(state => ({...state, ...filters}));
  }
}

Usage the store

To use the store, create an instance, initialize it, and then interact with its items:

// Creates a new store for managing products
const productsStore = new ProductsStore();
 
// Initializes the store and all its items
productsStore.initialize();
 
// Defines a side effect for the loading status changes
productsStore.isLoading
  .tap(isLoading => console.log('Loading state:', isLoading));
 
// Subscribes to the state observable to react to data updates
productsStore.data.$
  .subscribe(data => console.log('Updated products:', data.products));
 
// Updates filters to trigger products reloading
productsStore.updateFilters({page: 2});
 
// Retrieves the current data synchronously
const data = productsStore.data();
console.log('Current products:', data.products);
 
// Completes the store and all its items when done
productsStore.complete();
Last updated on