Redux
原理
1. 單一數據源
all states ==>Store
- 隨着組件的復雜度上升(包括交互邏輯和業務邏輯),數據來源逐漸混亂,導致組件內部數據調用十分復雜,會產生數據冗余或者混用等情況。
- Store 的基本思想是將所有的數據集中管理,數據通過 Store 分類處理更新,不再在組件內放養式生長。
2. 單向數據流
dispatch(actionCreator) => Reducer => (state, action) => state
- 單向數據流保證了數據的變化是有跡可循且受控制的。
- 通過綁定 Store 可以確定唯一數據來源。
- actionCreator 通過 dispatch 觸發,使組件內事件調用邏輯清晰,具體的事件處理邏輯不用放在組件寫,保持 view 層的純凈。
- Reducer 通過判斷不同的 actionType 處理不同數據更新,保證數據有秩序更新。
React + Redux
Action
- actionType 定義操作類型
- actionCreator 定義操作具體執行函數
1. Action 基礎寫法
- actionType 提供給 Reducer 判斷動作類型
- actionCreator 為可調用的執行函數,必須返回 actionType 類型
// actionType
export const ACTION_TYPE = 'ACTION_TYPE';
// actionCreator
let actionCreator = (config) => {
return {
type: ACTION_TYPE, // 必須定義 type
config // 傳遞參數 => reducer
}
}
2. Action 異步解決方法
- redux-thunk 中間層做數據異步轉換
- redux-saga 使用 ES6 generator / yield
2.1 redux-thunk 使用方法
- redux-thunk 配置
redux-thunk 為獨立工具,需要另外安裝,通過 redux 提供的中間件 applyMiddleware ,綁定到 store 中。
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducers from '../reducers';
let store = createStore(
reducers,
applyMiddleware(thunk)
);
- Action 使用 redux-thunk
獲取數據方法在異步獲取數據后需要再次調用接收方法接收數據。
// 接收方法
let receiveSomething = (res) => {
return {
type: RECEIVE_SOME,
res
}
}
// 獲取數據方法
export let fetchSomething = (args) => {
return dispatch => {
return fetch(args).then((res) => {
return dispatch(receiveSomething(res))
})
}
}
Reducer
- 引入 Action 中定義好的 actionType
- 傳入 初始數據 和 actionType 后,返回更新數據
(initialState, action) => newState
Reducer 基礎寫法
1.依據不同執行 ActionType 直接更新狀態
import { ACTION_A, ACTION_B } from '../actions';
let initialState = { ... }
function example(state = initialState, action) {
switch(action.type) {
case ACTION_A:
return Object.assign({}, state, action.config)
case ACTION_B:
return Object.assign({}, state, action.config)
}
}
2.對 Action 傳遞的數據多加一層處理
let doSomething = (config) => {
let { a, b } = config;
// do something with a, b
return { a, b }
}
function example(state = initialState, action) {
switch(action.type) {
case ACTION_TYPE:
return Object.assign({},
state,
doSomething(action.config))
}
}
3.合並多個 Reducer
通過 redux 提供的 combineReducers 將不同處理邏輯的 reducer 合並起來。
import { combineReducers } from 'redux';
export default combineReducers({
reducerA,
reducerB
});
// or
export let reducer = (state = initialState, action) {
a: processA(state.a, action),
b: processB(state.b, action)
}
Store
1. 將 Store 綁定 React
使用 react-redux 提供的 Provider 可以將 Store 注入到 react 中。
Store 將合並后的 reducers 通過 createStore 創建,此外下面示例代碼還使用中間件加入了一層 react-thunk 處理。
import ReactDOM from 'react-dom';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux'
import thunk from 'redux-thunk';
import reducers from './reducers';
let store = createStore(
reducers,
applyMiddleware(thunk)
);
ReactDOM.render((
<Provider store={store}>
// ...
</Provider>
), document.querySelector('#app'));
2. 將 state 綁定到 Component
使用 react-redux 提供的 connect 方法 將組件和所需數據綁定。
需要注意的是,Store 創建時接收的是合並后的 reducers, 因此不同 reducer 上的處理數據綁定在了不同 reducer 對象上,而不是全部掛載在 Store 上。
mapStateToProps 將組件內部所需數據通過 props 傳入組件內部。更多綁定機制,具體可參考connect
import React, { Component } from 'react';
import { connect } from 'react-redux';
class ComponentA extends Component {
//...
}
let mapStateToProps = (state) => {
// attention !!!
let { reducerA, reducerB } = state;
return {
propA: reducerA.propA,
propB: reducerB.propB
}
};
export default connect(mapStateToProps)(ComponentA);
Component
1. 概念
React bindings for Redux embrace the idea of separating presentational and container components.
Redux 的 React 綁定庫包含了 容器組件和展示組件相分離 的開發思想。
- Presentational Components 展示型組件
- Container Components 容器型組件
展示型組件和容器型組件的區別在官方文檔中已經給出很詳細的解釋了,但是中文文檔的翻譯有誤,所以直接看英文比較更容易懂。
Presentational Components | Container Components | |
---|---|---|
Purpose | How things look (markup, styles) | How things work (data fetching, state updates) |
Aware of Redux | No | Yes |
To read data | Read data from props | Subscribe to Redux state |
To change data | Invoke callbacks from props | Dispatch Redux actions |
Are written | By hand | Usually generated by React Redux |
組件類型區分的模糊點在於怎么界定組件的內部功能規划。如果判定一個組件為展示型組件,那么它所需數據和處理方法都應該從父級傳入,保持組件內部“純凈”。
在實際開發中,一個組件的邏輯跟業務緊密相關。如果需要將數據和方法從外部傳入,那么父級組件所做的事情會很多,多重的子組件也會把父級邏輯弄亂,這就不是 redux 的初衷了。
中文文檔翻譯的意思是:容器組件應該為路由層面的組件,但這樣既不符合實際開發需要,也違背了 redux 思想。真正界定兩種組件的因素是:
- 展示型組件: 類似純模板引擎,外加一層樣式渲染,只負責渲染從props傳進來的數據或者監聽事件和父組件做小聯動。它是“純凈”的,不需要使用到 Redux 的一套規則。
- 容器型組件: 需要異步獲取數據,更新組件狀態等等。需要跟業務邏輯打交道的組件都可以認為是容器組件。這些邏輯的復雜性需要將數據整合到 Store 里統一管理。
2. Component 基礎寫法
- 組件渲染完成后調用Action
當組件 connect 后,dispatch 方法已經注入到 props 中,所以觸發 Action 可以從 props 獲取 dispatch 方法。
import React, { Component } from 'react';
// actionCreator
import { actionA, actionB } from 'actions/actionA'
class ComponentA extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
let { dispatch } = this.props;
dispatch(actionA())
}
}
export default connect()(ComponentA);
- 組件模板內調用Action
組件內部所需的渲染數據都已經綁定在了 props 上,直接獲取即可。
需要注意的是,在事件監聽中觸發 Action,需要用一個匿名函數封裝,否則 React 在渲染時就會執行事件綁定事件,而不是當事件發生再執行。
render() {
let { dispatch, propA, propB } = this.props;
return (
<section>
// Attention !!!
<input type="text" onClick={(ev) => dispatch(actionB(ev))} />
<p className={propA}>{propB}</p>
</section>
)
}
- 容器組件傳遞方法
容器型組件需要連接 Redux,使用 dispatch 觸發 actionCreator。
展示型組件需要用到的方法調用在容器型組件內定義好,通過 props 傳入到展示型組件中。
// get actionCreator
import { actionA } from './actions/actionA';
class Parent extends Component {
handleCallback(data) {
// use dispatch
let { dispatch } = this.props;
dispatch(actionA(data));
}
render() {
return (
<Child onSomethingChange={this.handleCallback} />
)
}
}
// connet Redux
export default connect()(Parent);
- 展示組件接收props
展示型組件不需要用到 Redux 的一切,它的 props 僅僅存在於父級傳入的數據和方法。
// don't need action/dispatch/connect
class Child extends Component {
handleSomething(data) {
// handle anything with props
this.props.onSomethingChange(data);
}
render() {
return (
// just markup & style
<input onChange={handleSomething} />
)
}
}
Conclusion
圖示箭頭代表各概念之間的相互關系,不代表數據流。( 能理解下面這張圖,這篇文章就沒白看了 -。- )
參考文檔
END.