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!

State management in Angular with observable store services

By George Byte

State management in Angular with observable store services

  • 1,623