Creating an Extendable Basic State Store

Published on

Managing State in Angular with Rxjs

Managing state can be a challenage in many applications. For larger Angular projects there are libraries to solve this problem that you may have used before to manage your state; NGRX, NGSG, and a few others. These libraries are great options for larger project but can be a little time consuming to use when building small applications. One thing I’ve been looking for is a more simplified pattern for managing state in smaller Angular projects.

Below I’m be covering a solution that I’ve been using in my smaller Angular projects.

abstract-store.ts

import { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';

export abstract class AbstractStore<T> {
  protected state: T;
  protected stateSource: BehaviorSubject<T>;

  constructor(initialState: T) {
    this.state = this._deepFreeze(initialState);
    this.stateSource = new BehaviorSubject<T>(this.state);
  }

  public setState(state: T): void {
    this.state = this._deepFreeze(state);
    this.stateSource.next(this.state);
  }

  public select<R>(fn: (state: T) => R): Observable<R> {
    return this.stateSource
      .asObservable()
      .pipe(map(fn), distinctUntilChanged());
  }

  public abstract resetState(): void;

  private _deepFreeze<T>(obj: T): T {
    const propNames = Object.getOwnPropertyNames(obj);
    for (let name of propNames) {
      let value = (obj as any)[name];
      if (value && typeof value === 'object') {
        this._deepFreeze(value);
      }
    }
    return Object.freeze(obj);
  }
}

The AbstractStore is an abstract class that manages the vaules using a Rxjs BehaviorSubject. The basic concept is to store the current state object providing methods to both set/update your state and select a piece of state.

The setState will take the passed in state, freeze it making it immutable, then set it as our new state. By making the state immutable, we prevent unintentional side effects from mutating the state from outside the store. The only way to change the state is to use the store itself.

The select method will allow us to grab a piece of state as an Observable. It takes in a mapping function to select the piece of state we want from the state object. Notice the distinctUntilChanged pipe operator. This will make our selected state only emit new values when the reference to that value we want changes. Without this pipe operator, the Observable would emit any time the store changes regardless of whether our piece of state changed or not.

We can now use our AbstractStore by extending it from another class. Below is an example of a TodosStore

todos.store.ts

import { Injectable } from '@angular/core';
import { AbstractStore } from './abstract-store';

export interface Todo {
  id: number;
  todo: string;
  isComplete: boolean;
}

export interface TodosState {
  todos: Todo[];
}

const initialTodosState: TodosState = {
  todos: [],
};

@Injectable({
  providedIn: 'root',
})
export class TodosStore extends AbstractStore<TodosState> {
  constructor() {
    super({ ...initialTodosState });
  }

  public addTodo(todo: Todo): void {
    this.setState({
      ...this.state,
      todos: [todo, ...this.state.todos],
    });
  }

  public updateTodo(todo: Todo) {
    this.setState({
      ...this.state,
      todos: this.state.todos.map((t) => (t.id === todo.id ? todo : t)),
    });
  }

  public deleteTodo(todo: Todo) {
    this.setState({
      ...this.state,
      todos: this.state.todos.filter((t) => t.id !== todo.id),
    });
  }

  public resetState(): void {
    this.setState({ ...initialTodosState });
  }
}

Now we can inject our TodosStore in a component and access/mutate state values…

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit {
  public todos$: Observable<Todo[]>;

  constructor(private _todosStore: TodosStore) {}

  ngOnInit(): void {
    this.todos$ = this._todosStore
      .select((state) => state.todos)
      .pipe(map((todos) => todos.filter((todo) => !todo.isComplete)));
  }

  public createTodo(todo: Todo): void {
    this._todosStore.addTodo(todo);
  }

  public updateTodo(todo: Todo): void {
    this._todosStore.updateTodo(todo);
  }

  public deleteTodo(todo: Todo): void {
    this._todosStore.deleteTodo(todo);
  }
}

Our todo example in a working Stackblitz

comments powered by Disqus