State management in Angular with

observable store services

Jure Bajt,

full-stack engineer at Zemanta

jurebajt.com

Zemanta's dashboard

Hybrid Angular app (AngularJS + Angular).

 

AngularJS part stores some state in controllers and other in services (pub-sub pattern)​​.

No clear conventions set about state management

=

Hard to keep state consistent across all components and services

Main ideas of Redux

  • One source of truth (app state).
  • State is modified in a “pure” way via reducers.
  • Reducers are invoked by emitting events to them.
  • Interested entities are notified about state updates.

Redux is cool but ...

... it's not a silver bullet and it's complex.

Observable store pattern

  • main ideas from Redux
  • Angular's provider dependency injection
  • RxJS observables

Example app: Coffee elections

Abstract Store class

import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Observable';

export class Store<T> {
  private _state$: BehaviorSubject<T>;

  protected constructor (initialState: T) {
    this._state$ = new BehaviorSubject(initialState);
  }

  get state$ (): Observable<T> {
    return this._state$.asObservable();
  }

  get state (): T {
    return this._state$.getValue();
  }

  setState (nextState: T): void {
    this._state$.next(nextState);
  }
}

Abstract Store class provides a unified interface for all store services in an app to extend.

Feature specific stores

@Injectable()
export class CoffeeElectionStore extends Store<CoffeeElectionState> {
  constructor () {
    super(new CoffeeElectionState());
  }
}

Angular injectable service:

export class CoffeeElectionState {
  candidates: {name: string, votes: number}[] = [];
}

State object type definition and initial values:

CoffeeElectionStore inherits the functionality to update its state and get the current state or an observable of state.

Add custom state modifying methods ("reducers")

@Injectable()
export class CoffeeElectionStore extends Store<CoffeeElectionState> {
  constructor () {
    super(new CoffeeElectionState());
  }

  addVote (candidate: {name: string, votes: number}): void {
    this.setState({
      ...this.state,
      candidates: this.state.candidates.map(c => {
        if (c === candidate) {
          return {...c, votes: c.votes + 1};
        }
        return c;
      })
    });
  }

  addCandidate (name: string): void {
    this.setState({
      ...this.state,
      candidates: [...this.state.candidates, {name: name, votes: 0}]
    });
  }
}

Impossible to modify the state without notifying listeners about the change​.

Types of stores

  • global stores containing globally used state
  • component stores containing the state used by a single component

Split the app state into smaller chunks to make extending it with new features easier.

Global stores

@NgModule({
  ...
  providers: [ExampleGlobalStore],
})
export class AppModule {
  ...
}

Listed in module's providers list to add a singleton provider to Angular's dependency injector:

Using a global store:

@Component({ ... })
export class ExampleComponent {
  constructor (private exampleGlobalStore: ExampleGlobalStore) {
    // ExampleComponent has access to global state via
    // exampleGlobalStore reference
  }
}

Angular injects the same instance of a global store into every component/service depending on it!

Private components' stores

@Component({
  ...
  providers: [ExampleComponentStore],
})
export class ExampleComponent {
  ...
}

Listed in component's providers list:

Using a private component store:

@Component({ ... })
export class ExampleComponent {
  constructor (private exampleComponentStore: ExampleComponentStore) {
    // ExampleComponent has access to private state via
    // exampleComponentStore reference
  }
}

Private components' stores are not singletons!

Subscribing to state updates

@Component({ ... })
export class CoffeeElectionComponent implements OnInit {
  constructor (private store: CoffeeElectionStore) {}

  ngOnInit () {
    this.store.state$.subscribe(state => {
      // Logic to execute on state update
    });
  }
}

Subscribing to updates on a subset of state:

this.store.state$
  .map(state => state.candidates)
  .distinctUntilChanged()
  .subscribe(candidates => {
    // Logic to execute on state.candidates update
  });

Clean-up the subscriptions before components are removed from the DOM! (http://goo.gl/r6uyEy)

Async pipe

<ul>
  <li *ngFor="let candidate of (store.state$ | async).candidates">
    <span>{{ candidate.name }}</span>
    <span>Votes: {{ candidate.votes }}</span>
    <button (click)="store.addVote(candidate)">+</button>
  </li>
</ul>

Angular unsubscribes from subscriptions via async pipes automatically upon destroying the component.

Observable store pattern is based on a simple concept, but it provides an effective state management solution even in larger Angular applications without introducing additional complexity.

jurebajt.com

Thank you!