Last active
November 29, 2016 07:37
-
-
Save Hendrixer/38a08f389a314bfe580bd839f74430a0 to your computer and use it in GitHub Desktop.
Simple reactive store
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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