Elaine's Blog 朝著 senior 前進的工程師

Redux 單向資料流 學習筆記

2018-09-23

Redux 三個原則

  1. 單一事實來源
  2. state 只可讀
  3. 利用純函數來改變

基礎

action

  • 改變 state 的唯一辦法,就是使用 action
  • action 是一個 object
    • type 屬性是必要的
    • 其他屬性可以自由設置
const ADD_TODO = "ADD_TODO";

const action = {
  type: ADD_TODO,
  text: "Build my first Redux app"
};

action creator

  • view 要發送多少種消息,就會有多少種 action,如果都手寫,會很麻煩,可以定義一個函數來生成 action,這個函數就叫 action creator
const ADD_TODO = "ADD_TODO";

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  };
}

const action = addTodo("Learn Redux");

addTodo 函數就是一個 Action Creator

reducer

  • store 收到 action 以後,必須給出一個新的 state,這樣 view 才會發生變化,這種 state 的計算過程就叫做 reducer
  • 公式:之前的狀態 + 動作 = 新的狀態
  • 是純函數,給定一樣的輸入,必須有一樣的輸出,沒有 side effect
    1. 不能改變傳進來的參數
    2. 不執行有 side effect 的動作,像是呼叫 API 和 routing 轉換
    3. 不能呼叫 Date.now() 或者 Math.random() 等不純的函數,因爲每次會得到不一樣的結果
import { ADD_TODO } from "./actions";

let initialState = {
  todos: []
};

function todoApp(state = initialState, action) {
  switch (action.type) {
    case ADD_TODO:
      return Object.assign({}, state, {
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false
          }
        ]
      });
    default:
      return state;
  }
}
  • 避免直接修改 state,應該回傳一個新的 state
Object.assign(state, newData) // 不要使用這個
Object.assign({}, state, newData)    // 使用這個,這樣才不會覆蓋舊的 state
{ ...state, ...newData }    // 或是使用 es6 的 object spread spread operator

store

  • 整個應用只有一個 store,它是一個樹狀結構的物件
  • store 不是 class,它只是有幾個方法的 object

Redux api

createStore(reducer, [preloadedState], enhancer)

  • 創建一個 Redux store 來以存放應用中所有的 state
  • 輸入參數
    • reducer
      • 型態為 function
      • 輸入為當前的 state 樹、要處理的 action
      • 輸出新的 state 樹
    • [preloadedState] ????
      • 型態為 any
      • 在同構應用中,你可以決定是否把服務端傳來的 state 水合(hydrate)後傳給它,或者從之前保存的用戶會話中恢復一個傳給它
      • 如果你使用 combineReducers 創建 reducer,它必須是一個普通對象,與傳入的 keys 保持同樣的結構。否則,你可以自由傳入任何 reducer 可理解的內容
    • enhancer ????
      • 型態為 function
      • Store enhancer 是一個組合 store creator 的高階函數,返回一個新的強化過的 store creator。這與 middleware 相似,它也允許你通過複合函數改變 store 接口
  • 輸出
    • 保存了應用所有 state 的 object
  • 注意
    • 應用中不要創建多個 store!相反,使用 combineReducers 來把多個 reducer 創建成一個根 reducer
    • 對於服務端運行的同構應用,為每一個請求創建一個 store 實例,以此讓 store 相隔離。dispatch 一系列請求數據的 action 到 store 實例上,等待請求完成後再在服務端渲染應用 ????
    • 要使用多個 store 增強器的時候,你可能需要使用 compose ???
import { createStore } from "redux";
import todoApp from "./reducers";
const store = createStore(todoApp);

store.getState()

  • 返回應用當前的 state 樹
import { createStore } from "redux";
import todoApp from "./reducers";
const store = createStore(todoApp);
const state = store.getState();

store.dispatch(action)

  • 是觸發 state 變更的唯一方式
// 此檔案路徑為 ./actions/todoActions
export function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  };
}
import { createStore } from "redux";
import todoApp from "./reducers";
import { addTodo } from "./actions/todoActions";

const store = createStore(todoApp);
store.dispatch(addTodo("Learn Redux"));

store.subscribe(listener)

  • 綁定監聽器,一旦 state 發生變化,就自動執行 listener
  • 輸入參數
    • listener
      • 型態是 function
      • 一旦 state 發生變化就會執行此 callback
import { createStore } from "redux";
import todoApp from "./reducers";
const store = createStore(todoApp);
store.subscribe(listener);
  • 注意

    • 避免在 listener 函數中呼叫 dispatch(),可能會造成無限迴圈
      • 只有在回應使用者的行為或是特定條件下(例如,當 store 有特定欄位時 dispatch 一個 action),listener 才會呼叫 dispatch()
  • 解除綁定的監聽器

    • store.subscribe 方法會返回一個函數,調用這個函數就可以解除監聽
let unsubscribe = store.subscribe(listener);
unsubscribe();

combineReducers(reducers)

  • 隨著你的應用程式成長的更複雜,你會想要把你的 reducing function 拆分成各別的 function,每一個管理獨立的某部分 state
  • 這時可以使用 combineReducers 將多個 reducer 組合成一個根 reducer
  • 輸入參數
    • reducers
      • 型態為 object
      • 一個每個值都對應到不同的 reducing function 的物件,這些 reducing function 需要被合併成一個
      • 傳遞給 combineReducers 的 reducer 必須滿足下列條件
        • 所有未匹配到的 action,必須把它接收到的第一個參數也就是那個 state 原封不動返回
        • 永遠不能返回 undefined,當過早 return 時非常容易犯這個錯誤,為了避免錯誤擴散,遇到這種情況時 combineReducers 會拋出異常
        • 如果傳入的 state 就是 undefined,一定要返回對應 reducer 的初始 state
          • 根據上一條規則,初始 state 禁止使用 undefined
          • 使用 ES6 的默認參數值語法來設置初始 state 很容易,但你也可以手動檢查第一個參數是否為 undefined
  • 注意
    • 即使你通過 Redux.createStore(combineReducers(…), initialState) 指定初始 state,combineReducers 也會嘗試通過傳遞 undefined 的 state 來檢測你的 reducer 是否符合規則
    • 在 reducer 層級的任何一級都可以調用 combineReducers,並不是一定要在最外層,實際上,你可以把一些複雜的子 reducer 拆分成單獨的孫子級 reducer,甚至更多層
// reducers/todos.js
export default function todos(state = [], action) {
  switch (action.type) {
    case "ADD_TODO":
      return state.concat([action.text]);
    default:
      return state;
  }
}
// reducers/counter.js
export default function counter(state = 0, action) {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;
    case "DECREMENT":
      return state - 1;
    default:
      return state;
  }
}
// reducers/index.js
import { combineReducers } from "redux";
import todos from "./todos";
import counter from "./counter";

export default combineReducers({
  todos: todos,
  counter: counter
});

使用 ES6 property shorthand notation 簡化

export default combineReducers({
  todos,
  counter
});

React-Redux api

<Provider store>

  • 使子元件中的 connect() 函數都能夠獲得 Redux store
  • 通常綁在根元件
import React from "react";
import { render } from "react-dom";
import { Provider } from "react-redux";
import { createStore } from "redux";
import App from "./components/App";
import rootReducer from "./reducers";

const store = createStore(rootReducer);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])(<Component>)

  • 連接 React 元件與 Redux store
  • 輸入參數
    • mapStateToProps
      • 型態為 function
      • 任何時候,只要 Redux store 發生改變,mapStateToProps 函數就會被調用,如果你省略了這個參數,你的元件將不會監聽 Redux store
      • mapStateToProps(state, [ownProps]): stateProps
        • ownProps 是傳遞到元件的 props,只要元件收到新的 props,mapStateToProps 函數也會被調用
        • stateProps 必須為 object,此 object 會與元件的 props 合併
    • mapDispatchToProps
      • 型態為 object 或 function
      • 如果型態為 object
        • 每個定義在該 object 的函數都將被當作 Redux action creator,object 所定義的方法名將作為屬性名
        • 每個方法都會回傳一個新的函數,這新的函數就是以 Redux action creator 的回傳值作為參數的 dispatch 函數,這些屬性都會被合併到元件的 props 中
      • 如果型態為 function
        • 該函數將接收一個 dispatch 函數,然後由你來決定如何回傳一個 object,這個 object 通過 dispatch 函數與 action creator 以某種方式綁定在一起
          • 也許會用到 Redux 的輔助函數 bindActionCreators()
      • 如果型態為空
        • 這會直接把 dispatch 映對到 props 上,你想執行 store.dispatch(action),相當於使用 this.props.dispatch(action)
    • mergeProps
      • 型態為 function
      • 會讓 mapStateToProps() 與 mapDispatchToProps() 的執行結果和組件自身的 props 將傳入到這個 callback 中
      • 該 callback 返回的 object 將作為 props 傳遞到被包裝的組件中。你也許可以用這個 callback,根據組件的 props 來篩選部分的 state 數據,或者把 props 中的某個特定變量與 action creator 綁定在一起
      • 如果你省略這個參數,默認情況下返回 Object.assign({}, ownProps, stateProps, dispatchProps) 的結果。
      • mergeProps(stateProps, dispatchProps, ownProps): props
    • options
      • 型態為 object
      • 可以定制 connector 的行為
      • pure
        • 型態為 Boolean
        • 預設值為 true
        • 如果為 true,connector 將執行 shouldComponentUpdate 並且淺對比 mergeProps 的結果,避免不必要的更新,前提是當前組件是一個”純”組件,它不依賴於任何的輸入或 state 而只依賴於 props 和 Redux store 的 state
      • withRef
        • 型態為 Boolean
        • 預設值為 false
        • 如果為 true,connector 會保存一個對被包裝組件實例的引用,該引用通過 getWrappedInstance() 方法獲得
  • 輸出
    • 一個新的已與 Redux store 連接的元件類別
import { connect } from "react-redux";

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList);

參考資料

Flux 數據流兩三事兒

Redux 中文文档

Day 27: Redux 篇 - 使用 react-redux 綁定 Redux 與 React

ReactJS / Redux Tutorial - #1 Introduction


Similar Posts

Content