iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 13
0
Modern Web

ngrx/store 4 學習筆記系列 第 13

[ngrx/store-13] Store 加入 select

Store 加入 select

我們的 Store 內部大致完成,但是對於客戶 (Subscriber),我們還少了介面,在 ngrx/store 裡,給客戶的介面是用 store.select().select() 最主要的目的是提供 Subscriber 它所要的狀態,而不是整個狀態樹,它是一個 Observable,客戶 subscribe() 後會被 push 新的狀態。

加入 Select

我們知道 Store 是一個 BehaviorSubject,它隨時會紀錄最新的狀態樹,而狀態樹用 key 來做分類,所以做 select() 其實很簡單,只要用一個 .map() Operator如下:

class Store extends Rx.BehaviorSubject<Action> {
    // 省略 ...
  select(key:string) {
    return this.map(state=> state[key]);
  }
}
// 省略 ...
// add subscriber
const sub1 = store.select('login')
    .subscribe(s => console.log('login status => ', s));

const sub2 = store.select('messages')
    .subscribe(s => console.log('messages => ', s));

// start dispatch action about login
store.dispatch({type: 'LOGIN'});   
store.dispatch({type: 'LOGOUT'});  
store.dispatch({type: 'LOGIN'});   

// start dispatch about messages
store.dispatch({type: 'ADD_MESSAGE', payload: {id: 1, msg: 'First Message'}});
store.dispatch({type: 'ADD_MESSAGE', payload: {id: 2, msg: 'Second Message'}});
store.dispatch({type: 'REMOVE_MESSAGE', payload: 1});

這裡假設 sub1只對 login 有興趣,而 sub2 只對 messages 有興趣,但是我們來看一下輸出

https://ithelp.ithome.com.tw/upload/images/20171229/20103574bAySwm6BQP.png

這種做法的話,每次 store dispatch 一個 Action 時,sub1sub2 都會得到一個值,而對於 sub2 而言,login 有關的 Action 並不會帶給 messages 新的值, 所以一剛開始時,sub2 一直收到 [] 的狀態,這不是它想要的。

在 Observable 中有一個 Operator .distinctUntilChange(),它會根據新的 .next() 做過濾,如果值不變,它會將它過濾掉,不會丟給 Subscriber。

Select with distinctUntiChange

我們加入 .distinctUntilChange().select()

class Store extends Rx.BehaviorSubject<Action> {
    // 省略 ...
  select(key:string) {
    return this.map(state=> state[key])
      .distinctUntilChanged();
  }
}

結果如下
https://ithelp.ithome.com.tw/upload/images/20171229/20103574iAM2zZMltX.png

果然清爽許多,sub1sub2 都會得到它們想要的結果。

完整程式

interface Action {
  type: string;
  payload?: any
}

class Dispatcher extends Rx.Subject<Action> {
  dispatch(act) {
      this.next(act);
  }
}

class Store extends Rx.BehaviorSubject<Action> {
  constructor(private dispatcher, private reducer, initialState) {
    super(initialState);
    this.dispatcher
      .do((v) => { /*console.log('do some effect for', v.type) */ })    
      .scan((s, v) => this.reducer(s, v), initialState)
      .subscribe(state => {
        super.next(state);  // new state, push to subscriber
      });
  }
  dispatch(act) {          // delegate to dispatcher
    this.dispatcher.dispatch(act);
  }
  // override next to allow store subscribe action$
  next(act) {
    this.dispatcher.dispatch(act);
  }
  select(key:string) {
    return this.map(state=> state[key])
      .distinctUntilChanged();
  }
}

const login = (state = false, action) => {
  switch (action.type) {
    case 'LOGIN':
      return true;
    case 'LOGOUT':
      return false;
    default:
      return state;
  }
}

const messages = (state = [], action) => {
  switch (action.type) {
    case 'ADD_MESSAGE': 
      return [...state, action.payload];
    case 'REMOVE_MESSAGE':
      return state.filter(msg => msg.id != action.payload);
    default:
      return state;
  }
}

const myReducer = {login, messages};       // object of two reducer functions
const combineReducer = reducers => (state, action) => {
  return Object.keys(reducers).reduce((nextState, key) => {
    nextState[key]=reducers[key](state[key], action);
    //console.log('nextState is ', nextState);
    return nextState;
  }, {})
}

const rootReducer = combineReducer(myReducer);

// instanciate new store with initialstate
const initialState = {login: false, messages: []};
const dispatcher = new Dispatcher();
const store = new Store(dispatcher, rootReducer, initialState);

// add subscriber
const sub1 = store.select('login')
    .subscribe(s => console.log('login status => ', s));

const sub2 = store.select('messages')
    .subscribe(s => console.log('messages => ', s));

// start dispatch action about login
store.dispatch({type: 'LOGIN'});   
store.dispatch({type: 'LOGOUT'});  
store.dispatch({type: 'LOGIN'});   

// start dispatch about messages
store.dispatch({type: 'ADD_MESSAGE', payload: {id: 1, msg: 'First Message'}});
store.dispatch({type: 'ADD_MESSAGE', payload: {id: 2, msg: 'Second Message'}});
store.dispatch({type: 'REMOVE_MESSAGE', payload: 1});

codepen

至此,我們已經將 Observable 跟 store 介紹差不多了, ngrx/store 有三大主軸, Angular (ng), Observable (rx) 跟 Store (flux),接下來我們要進入 Angular 主題來應用 ngrx/store。


上一篇
[ngrx/store-12] Store 加完整的 Reducer
下一篇
[ngrx/store-14] 用 Angular 5  建立一個投顧網站 - 需求篇
系列文
ngrx/store 4 學習筆記30

尚未有邦友留言

立即登入留言