Redux 三個原則
- 單一事實來源
- state 只可讀
- 利用純函數來改變
基礎
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
- 不能改變傳進來的參數
- 不執行有 side effect 的動作,像是呼叫 API 和 routing 轉換
- 不能呼叫 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 接口
- reducer
- 輸出
- 保存了應用所有 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
- listener
import { createStore } from "redux";
import todoApp from "./reducers";
const store = createStore(todoApp);
store.subscribe(listener);
-
注意
- 避免在 listener 函數中呼叫 dispatch(),可能會造成無限迴圈
- 只有在回應使用者的行為或是特定條件下(例如,當 store 有特定欄位時 dispatch 一個 action),listener 才會呼叫 dispatch()
- 避免在 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
- reducers
- 注意
- 即使你通過 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 函數,然後由你來決定如何回傳一個 object,這個 object 通過 dispatch 函數與 action creator 以某種方式綁定在一起
- 如果型態為空
- 這會直接把 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() 方法獲得
- mapStateToProps
- 輸出
- 一個新的已與 Redux store 連接的元件類別
import { connect } from "react-redux";
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList);