3 minutes
A State Store with Angular Signals
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.