Skip to content

Instantly share code, notes, and snippets.

@cavebatsofware
Created August 31, 2023 20:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cavebatsofware/53c76ad338f9ac869d8a09720f40d15e to your computer and use it in GitHub Desktop.
Save cavebatsofware/53c76ad338f9ac869d8a09720f40d15e to your computer and use it in GitHub Desktop.
BasicStateService

The AppBasicStateService provides an easy way to maintain a state for a specific part of the application, allowing components to read from and update that state. The examples below will cover:

  1. Initializing the state in a component.
  2. Updating the state in a component.
  3. Subscribing to state changes in a component.
  4. Unsubscribing and cleanup.

Usage Documentation for AppBasicStateService


1. Initializing State in a Component

In the component's constructor or lifecycle method, you can initialize a state by calling initState() on the AppBasicStateService and providing an identifier and initial state.

constructor(private basicStateService: AppBasicStateService) {
  this.basicStateService.initState(
    RegisteredStates.SFTP_IMPORT_LIST_FILTERS,
    initialFilters
  );
}

2. Updating the State in a Component

To update the state, you'd use the setState() method, passing the state identifier and the new state.

onChangeFilters(newFilters: ISftpImportListFilters): void {
  this.basicStateService.setState(
    RegisteredStates.SFTP_IMPORT_LIST_FILTERS,
    newFilters
  );
}

3. Subscribing to State Changes in a Component

To react to changes in the state, you can subscribe to the state in the component's lifecycle methods (e.g., ngOnInit, ngAfterViewInit).

filterSubscription: Subscription;

ngAfterViewInit(): void {
  this.filterSubscription = this.basicStateService
    .getState(RegisteredStates.SFTP_IMPORT_LIST_FILTERS)
    .subscribe((newFilter) => {
      // Handle the new filter value
      this.currentFilter = newFilter;
    });
}

ngOnDestroy(): void {
  this.filterSubscription.unsubscribe();
  this.basicStateService.unregisterState(
    RegisteredStates.SFTP_IMPORT_LIST_FILTERS
  );
}

Using takeUntil

This is an alternative approach to the one above, which uses the takeUntil RxJS operator.

Why is this setup different?

We are using the takeUntil RxJS operator, which ensures that the subscription is automatically unsubscribed when this.destroy$ emits a value. This is a common pattern in Angular to handle unsubscriptions and avoid memory leaks. The this.destroy$ is a Subject, and when the component's ngOnDestroy lifecycle method is called, we emit a value with this.destroy$.next(), effectively cleaning up any subscriptions. This is mentioned further in the "Unsubscribing and Cleanup" section below.

public ngAfterViewInit(): void {
  this.basicStateService
    .getState(RegisteredStates.SFTP_IMPORT_LIST_FILTERS)
    .pipe(takeUntil(this.destroy$))
    .subscribe(async (newFilter) => {
      if (newFilter) {
        await this.updateFilters(newFilter);
      }
    });
}

public ngOnDestroy(): void {
  this.destroy$.next();
  this.destroy$.complete();
  this.basicStateService.unregisterState(
    RegisteredStates.SFTP_IMPORT_LIST_FILTERS
  );
}

4. Unsubscribing and Cleanup

It's good practice to unsubscribe from Observables to prevent memory leaks. In the ngOnDestroy lifecycle hook, ensure you unsubscribe. Both the takeUntil and Subscription approaches are below. You can use either approach, but the takeUntil approach can be more flexible if you need to unsubscribe from multiple Observables.

private filterSubscription: Subscription;
ngOnDestroy(): void {
  this.filterSubscription.unsubscribe();
  this.basicStateService.unregisterState(
    RegisteredStates.SFTP_IMPORT_LIST_FILTERS
  );
}

private destroy$: Subject<void> = new Subject<void>();
public ngOnDestroy(): void {
  this.destroy$.next();
  this.destroy$.complete();
  this.basicStateService.unregisterState(
    RegisteredStates.SFTP_IMPORT_LIST_FILTERS
  );
}

Best Practices:

  • Initialization: It's usually best to initialize the state early in the component's lifecycle (e.g., in the constructor or ngOnInit).
  • Error Handling: The service has built-in error handling, which will log errors if you attempt to update a state that hasn't been initialized. Make sure to look for these errors while developing and add any needed try/catch blocks.
  • Cleanup: Always unsubscribe from any Observables and deregister any states in the ngOnDestroy method to prevent memory leaks.
    • The component responsible for initializing the state should be the one to deregister the state in the ngOnDestroy method.
  • State Identifiers: Use the RegisteredStates enum to register state identifiers. This will help prevent typos and make it easier to find where a state is registered.

I hope this helps! Adjust as needed for your specific use case.

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { logger } from '../logger';
export enum RegisteredStates {
SFTP_IMPORT_LIST_FILTERS = 'sftpImportListFilters'
}
@Injectable({
providedIn: 'root'
})
export class AppBasicStateService {
private states: Map<string, BehaviorSubject<any>> = new Map();
private defaultState = {};
// Initialize filter state for a table
initState(stateId: string, initialState: any) {
if (!this.states.has(stateId)) {
this.states.set(stateId, new BehaviorSubject<any>(initialState));
} else {
const message = `State already initialized for identifier: ${stateId}.`;
logger.error(message);
throw new Error(message);
}
}
// For components to set a new filter state for a specific table
setState(stateId: string, newState: any) {
if (this.states.has(stateId)) {
this.states.get(stateId).next(newState);
} else {
const message =
'State not initialized for identifier: ' +
`${stateId} while attempting to set new state.`;
logger.error(message);
throw new Error(message);
}
}
getState(identifier: string): Observable<any> {
if (this.states.has(identifier)) {
return this.states.get(identifier).asObservable();
} else {
logger.warn(
'No state found for identifier: ' +
`${identifier} while attempting to retrieve.`
);
return of(this.defaultState);
}
}
unregisterState(identifier: string) {
if (this.states.has(identifier)) {
this.states.delete(identifier);
} else {
logger.warn(
'No state found for identifier: ' +
`${identifier} while attempting to deregister.`
);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment