Skip to content

Instantly share code, notes, and snippets.

@Hendrixer
Last active November 29, 2016 07:37
Show Gist options
  • Save Hendrixer/38a08f389a314bfe580bd839f74430a0 to your computer and use it in GitHub Desktop.
Save Hendrixer/38a08f389a314bfe580bd839f74430a0 to your computer and use it in GitHub Desktop.
Simple reactive store
Angular 2 uses observables for many features and has a peer dependency on [RxJs](http://reactivex.io/rxjs/) for its robust API around observables. The community has adopted a single store approach for dealing with state in modern applications. Let's build a store for our Angular applications that is both reactive and easy to use with RxJs.
## Single store is awesome
We'll create this store with intentions on it being the only store in our app. By doing this, we can provide a better experience and lower the difficulty of reasoning about state in our app, because all the state is in one place! First we'll create the [provider](https://angular.io/docs/ts/latest/guide/dependency-injection.html#!#injector-providers) for the store itself.
```typescript
export class AppStore {}
```
## Add a subject
Right now our store literally does nothing. We want this store to have a reactive api that we can use in our application. We're going to create a [Subject](http://reactivex.io/rxjs/manual/overview.html#subject) using `RxJs`. A subject is perfect for our store because we can multicast to more than one observer. This just means we can setup many listeners. This is going to allow use to subscribe to our store in other components and providers. We need the Subject to always know what the current state is at all times, and when a new observer subscribes, the Subject should provide the observer the current state. Luckily, there is a special Subject for this, a [Behavior Subject](http://reactivex.io/rxjs/manual/overview.html#behaviorsubject). Let's make our store reactive!
```typescript
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
const store = new BehaviorSubject();
export class AppStore {
store = store;
}
```
Above we created the store outside the class to ensure there will only ever be one instance of the actual store no matter how Angular injects and instantiates the `AppStore` provider. Now lets set a default value for our store.
```typescript
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
const state = {
user: {},
isLoading: false,
items: []
};
const store = new BehaviorSubject<state>(state);
export class AppStore {
store = store;
}
```
## Subscriptions
At this point, our `AppStore` is ready to be used in our app! Yea, that's basically it, surprised? Although we could stop here, we should make it super easy to work with the store in our application. The first thing we can do is setup a way to subscribe to store changes anywhere in our app.
```typescript
export class AppStore {
store = store;
changes = store.asObservable();
}
```
We converted the store to an observable so we can subscribe to it. The `changes` property is going to be that observable. Now all we have to do is subscribe to changes anywhere in our app and we'll be able to see those changes to the store. We should also have a way to get the current state at any given time without having to wait for changes.
```typescript
export class AppStore {
store = store;
changes = store.asObservable();
getState() {
return this.store.value;
}
}
```
A `BehaviorSubject` allows us to access the current state synchronously by using the `value` property. We can now subscribe to changes and access the current state, lets create an easy way to make those state changes to the store.
## Updates
We'll continue to make it easy to use our store in our app by creating a simple way to issue store updates. Because our state lives in the store, we can't just mutate the state and expect the store to update. You lose the benefit of a single store at that point and your app can get messy and confusing as it grows. First lets create a interface for our state.
```typescript
interface State {
user: Object;
isLoading: boolean;
items: any[];
}
```
Now that we have that interface, we can ensure that new store updates will always be the shape of our state. Next we'll create a method that when given a state, will update the store with that state.
```typescript
export class AppStore {
// ...
setState(state: State) { // use type here
// will trigger all subscriptions to this.changes
this.store.next(state);
}
}
```
Our store is looking pretty sweet now! So far we can subscribe to state changes on the store and create state changes. Lets add this store to a Component and use it!
## Using the store
Be sure to inject the `AppStore` before using it. Now, inside a component...
```typescript
import { Component } from '@angular/core';
import { Store } from './store';
import 'rxjs/Rx';
@Component({
selector: 'app',
template: `
<div *ngIf="isLoading">...loading</div>
`
})
class App {
isLoading: boolean = false;
constructor(private store: AppStore) {
this.store
.changes
.pluck('isLoading')
.subscribe((isLoading: boolean) => this.isLoader = isLoading)
}
}
```
Once we have the store, all we have to do is `subscribe` to the changes. We then use the `pluck` operator as shortcut to grab the `isLoading` prop from the store and bind it to the local state for templates. Now every time there is a call to `setState`, that subscribe callback will run.
```typescript
class App {
// ...
showLoader(isLoading: boolean) {
const currentState = this.store.getState();
currentState.isLoading = isLoading
this.store.setState(currentState);
}
}
```
The `updateName` method takes a name and updates `isLoading` in the current state with value, then calls setState on the store. This looks ok, and, it'll work, but its not what we want. We're directly mutating the state, and completely bypassing the store. Kinda defeats the purpose of a store in the first place. All state changes, even nested state, must go through the store. Lets make sure we can't mutate the state to ensure a nice unidirectional data flow where we have one store that pushes state changes to our app, and our app issuing those changes. Just one big circle.
```typescript
changes = store.asObservable().distinctUntilChanged()
```
By using `distinctUntilChanged`, the store won't push updates triggered by `setState` unless the state is an entirely different object. Now we have to change how our component updates the state in the store, because its current implementation won't trigger a change now.
```typescript
class App {
// ...
showLoader(isLoading: boolean) {
const currentState = this.store.getState();
this.store.setState(Object.assign({}, currentState, { isLoading }));
}
}
```
Using `Object.assign` or another merging strategy, we create a new object with the new value of `isLoading`. This state change will be pushed through. Because we're subscribing in the constructor, the component get's notified of this change and updates its local state. it all comes full circle! Another benefit of our immutable store, is that now we can take advantage of some performance enhancements like changing the change detection strategy for our components. I did promise a better dev flow with this single store, so without getting complicated with some sophisticated dev tools, lets create some middleware for logging!
```typescript
import 'rxjs/Rx';
export class AppStore {
store = store;
changes = store
.asObservable()
.distinctUntilChanged()
// log new state
.do(changes => console.log('new state', changes))
getState() {
return this.store.value;
}
setState(state: State) {
console.log('setState ', state); // log update
this.store.next(state);
}
}
```
That was easy. Using the `do` operator on changes, we can log the new state. Then inside of setState, we can log to see what new state is trying to set. These two values won't always be the same. Changes will only log immutable state changes, where setState will log any attempt to update the state. Very useful for debugging.
## Conclusion
All together now.
```typescript
import 'rxjs/Rx';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
const state = {
user: {},
isLoading: false,
items: []
};
interface State {
user: Object;
isLoading: boolean;
items: any[];
}
const store = new BehaviorSubject<state>(state);
export class AppStore {
store = store;
changes = store
.asObservable()
.distinctUntilChanged()
// log new state
.do(changes => console.log('new state', changes))
getState() {
return this.store.value;
}
setState(state: State) {
console.log('setState ', state); // log update
this.store.next(state);
}
}
```
```typescript
import { Component } from '@angular/core';
import { Store } from './store';
import 'rxjs/Rx';
@Component({
selector: 'app',
template: `
<div *ngIf="isLoading">...loading</div>
`
})
class App {
isLoading: boolean = false;
constructor(private store: AppStore) {
this.store
.changes
.pluck('isLoading')
.subscribe((isLoading: boolean) => this.isLoader = isLoading)
}
showLoader(isLoading: boolean) {
const currentState = this.store.getState();
this.store.setState(Object.assign({}, currentState, { isLoading }));
}
}
```
This is a great starting point for creating a solution for state management in your Angular 2 apps. There's so many more things we can do to make this easier, like getting rid of the boilerplate needed to perform the immutable state changes. You can take a look at our free [Angular 2 fundamentals course](http://courses.angularclass.com/courses/angular-2-fundamentals), where we do just that.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment