React+Redux入坑指南

照澄發表於2016-08-01

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 非同步解決方法

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.


相關文章