In Angular 16 Signals were introduced in developer preview. From the documentation:

“A signal is a wrapper around a value that can notify interested consumers when that value changes. Signals can contain any value, from simple primitives to complex data structures.”

Perfect! It seems I can use it in an State Store for change tracking state objects. So let’s start

A State Store with Angular Signals

First of all I need a type to hold some state. To keep it simple I will re-use the counter state which I have used before in some former State Store examples:

export type CounterState = {
    counter: number;
}

The next step is to implement a generic State Store base class using Signals. In the past I have created a similar one with the BehaviourSubject type of the RXJS library. I just refactored this a bit into:

import { Injectable, Signal, signal } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class StateStoreBase<T> {
  protected _state = signal<T>({} as T);

  public get currentValue(): Signal<T> {
    return this._state.asReadonly();
  }

  public nextValue(item: T) {
    this._state.set(item);
  }
}

By using Signals we don’t have a dependency on the RXJS library anymore. The signal needs to be initialized by some default: protected _state = signal<T>({} as T);

The readonly currentValue property returns the Signal. This property will be used in databinding later.

To submit a new counter value we can use the nextValue function.

The store actions are defined with an interface, for the CounterStateStore the actions are: increment, decrement and reset:

export interface CounterStateStoreActions {
    increment(): void;
    decrement(): void;
    reset(): void;
}

Now we can create the CounterState store which derives from the StateStoreBase and implements the CounterStateStoreActions:

import { Injectable } from '@angular/core';
import { CounterState } from '../state/counter-state';
import { CounterStateStoreActions } from '../state-actions/counter-state-store-actions';
import { StateStoreBase } from './state-store-base';

@Injectable({ providedIn: 'root' })
export class CounterStateStore 
  extends StateStoreBase<CounterState> 
  implements CounterStateStoreActions
  {
    protected constructor() {
      super();
      const initialState: CounterState = {counter: 0}; 
      this.nextValue(initialState);
    }

    public increment(): void {
      this.executeStateAction((state: CounterState) => state.counter++);
    }

    public decrement(): void {
      this.executeStateAction((state: CounterState) => state.counter--);
    }

    public reset(): void {
      this.executeStateAction((state: CounterState) => state.counter = 0);
    }

    private executeStateAction(action: Function) {
      const state: CounterState = this.currentValue();
      action(state);
      this.nextValue(state);
    }
}

Last step for now is to run some specs a verify it is working as expected:

import { TestBed } from '@angular/core/testing';

import { CounterStateStore } from './counter-state-store';
import { CounterState } from '../state/counter-state';

describe('CounterStateStore', () => {
  let stateStore: CounterStateStore;

  beforeEach(() => {
    TestBed.configureTestingModule({});
    stateStore = TestBed.inject(CounterStateStore);
  });

  it('should be created', () => {
    expect(stateStore).toBeTruthy();
  });

  it ('should initialize with zero', () => {
    const value: CounterState = stateStore.currentValue();
    expect(value.counter).toBe(0);
  });

  it ('increment should add one to counter', () => {
    stateStore.increment();
    stateStore.increment();
    stateStore.increment();

    const value: CounterState = stateStore.currentValue();
    expect(value.counter).toBe(3);
  });

  it ('decrement should subtract one to counter', () => {
    stateStore.decrement();
    stateStore.decrement();
    stateStore.decrement();

    const value: CounterState = stateStore.currentValue();
    expect(value.counter).toBe(-3);
  });

  it ('reset should reset counter', () => {
    stateStore.decrement();
    stateStore.decrement();
    stateStore.increment();
    stateStore.reset();

    const value: CounterState = stateStore.currentValue();
    expect(value.counter).toBe(0);
  });
});

And it is Green, Green, Green for now. In the next post I will use this Object Store in a view and use databinding to connect it to a user interface.