Jure Bajt,
full-stack engineer at Zemanta
jurebajt.com
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
Redux is cool but ...
... it's not a silver bullet and it's complex.
Example app: Coffee elections
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.
@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.
@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.
Split the app state into smaller chunks to make extending it with new features easier.
@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!
@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!
@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)
<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!