Redux 是「React 全家桶」中極為重要的一員,它試圖為 React 應用提供「可預測化的狀態管理」機制。Redux 本身足夠簡單,除了 React,它還能夠支持其他界面框架。所以如果要將 Redux 和 React 結合起來使用,就還需要一些額外的工具,其中最重要的莫過於 react-redux 了。
react-redux 提供了兩個重要的對象,Provider
和 connect
,前者使 React 組件可被連接(connectable),后者把 React 組件和 Redux 的 store 真正連接起來。react-redux 的文檔中,對 connect
的描述是一段晦澀難懂的英文,在初學 redux 的時候,我對着這段文檔閱讀了很久,都沒有全部弄明白其中的意思(大概就是,單詞我都認識,連起來啥意思就不明白了的感覺吧)。
在使用了一段時間 redux 后,本文嘗試再次回到這里,給這段文檔(同時摘抄在附錄中)一個靠譜的解讀。
1. 預備知識
首先回顧一下 redux 的基本用法。如果你還沒有閱讀過 redux 的文檔,你一定要先去閱讀一下。
const reducer = (state = {count: 0}, action) => { switch (action.type){ case 'INCREASE': return {count: state.count + 1}; case 'DECREASE': return {count: state.count - 1}; default: return state; } } const actions = { increase: () => ({type: 'INCREASE'}), decrease: () => ({type: 'DECREASE'}) } const store = createStore(reducer); store.subscribe(() => console.log(store.getState()) ); store.dispatch(actions.increase()) // {count: 1} store.dispatch(actions.increase()) // {count: 2} store.dispatch(actions.increase()) // {count: 3}
通過 reducer
創建一個 store
,每當我們在 store
上 dispatch
一個 action
,store
內的數據就會相應地發生變化。
我們當然可以直接在 React 中使用 Redux:在最外層容器組件中初始化 store
,然后將 state
上的屬性作為 props
層層傳遞下去。
class App extends Component{ componentWillMount(){ store.subscribe((state)=>this.setState(state)) } render(){ return <Comp state={this.state} onIncrease={()=>store.dispatch(actions.increase())} onDecrease={()=>store.dispatch(actions.decrease())} /> } }
但這並不是最佳的方式。最佳的方式是使用 react-redux 提供的 Provider
和 connect
方法。
2. 使用 react-redux
首先在最外層容器中,把所有內容包裹在 Provider
組件中,將之前創建的 store
作為 prop
傳給 Provider
。
const App = () => { return ( <Provider store={store}> <Comp/> </Provider> ) };
Provider
內的任何一個組件(比如這里的 Comp
),如果需要使用 state
中的數據,就必須是「被 connect 過的」組件——使用 connect
方法對「你編寫的組件(MyComp
)」進行包裝后的產物。
class MyComp extends Component { // content... } const Comp = connect(...args)(MyComp);
可見,connect
方法是重中之重。
3. connect 詳解
究竟 connect
方法到底做了什么,我們來一探究竟。
首先看下函數的簽名:
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
connect()
接收四個參數,它們分別是 mapStateToProps
,mapDispatchToProps
,mergeProps
和options
。
3.1. mapStateToProps(state, ownProps) : stateProps
這個函數允許我們將 store
中的數據作為 props
綁定到組件上。
const mapStateToProps = (state) => { return { count: state.count } }
這個函數的第一個參數就是 Redux 的 store
,我們從中摘取了 count
屬性。因為返回了具有 count
屬性的對象,所以 MyComp
會有名為 count
的 props
字段。
class MyComp extends Component { render(){ return <div>計數:{this.props.count}次</div> } } const Comp = connect(...args)(MyComp);
當然,你不必將 state
中的數據原封不動地傳入組件,可以根據 state
中的數據,動態地輸出組件需要的(最小)屬性。
const mapStateToProps = (state) => { return { greaterThanFive: state.count > 5 } }
函數的第二個參數 ownProps
,是 MyComp
自己的 props
。有的時候,ownProps
也會對其產生影響。比如,當你在 store
中維護了一個用戶列表,而你的組件 MyComp
只關心一個用戶(通過 props
中的 userId
體現)。
const mapStateToProps = (state, ownProps) => { // state 是 {userList: [{id: 0, name: '王二'}]} return { user: _.find(state.userList, {id: ownProps.userId}) } } class MyComp extends Component { static PropTypes = { userId: PropTypes.string.isRequired, user: PropTypes.object }; render(){ return <div>用戶名:{this.props.user.name}</div> } } const Comp = connect(mapStateToProps)(MyComp);
當 state
變化,或者 ownProps
變化的時候,mapStateToProps
都會被調用,計算出一個新的 stateProps
,(在與 ownProps
merge 后)更新給 MyComp
。
這就是將 Redux store
中的數據連接到組件的基本方式。
3.2. mapDispatchToProps(dispatch, ownProps): dispatchProps
connect
的第二個參數是 mapDispatchToProps
,它的功能是,將 action 作為 props
綁定到 MyComp
上。
const mapDispatchToProps = (dispatch, ownProps) => { return { increase: (...args) => dispatch(actions.increase(...args)), decrease: (...args) => dispatch(actions.decrease(...args)) } } class MyComp extends Component { render(){ const {count, increase, decrease} = this.props; return (<div> <div>計數:{this.props.count}次</div> <button onClick={increase}>增加</button> <button onClick={decrease}>減少</button> </div>) } } const Comp = connect(mapStateToProps, mapDispatchToProps)(MyComp);
由於 mapDispatchToProps
方法返回了具有 increase
屬性和 decrease
屬性的對象,這兩個屬性也會成為 MyComp
的 props
。
如上所示,調用 actions.increase()
只能得到一個 action
對象 {type:'INCREASE'}
,要觸發這個 action
必須在 store
上調用 dispatch
方法。diapatch
正是 mapDispatchToProps
的第一個參數。但是,為了不讓 MyComp
組件感知到 dispatch
的存在,我們需要將 increase
和 decrease
兩個函數包裝一下,使之成為直接可被調用的函數(即,調用該方法就會觸發 dispatch
)。
Redux 本身提供了 bindActionCreators
函數,來將 action 包裝成直接可被調用的函數。
import {bindActionCreators} from 'redux'; const mapDispatchToProps = (dispatch, ownProps) => { return bindActionCreators({ increase: action.increase, decrease: action.decrease }); }
同樣,當 ownProps
變化的時候,該函數也會被調用,生成一個新的 dispatchProps
,(在與 statePrope
和 ownProps
merge 后)更新給 MyComp
。注意,action
的變化不會引起上述過程,默認 action
在組件的生命周期中是固定的。
3.3. [mergeProps(stateProps, dispatchProps, ownProps): props]
之前說過,不管是 stateProps
還是 dispatchProps
,都需要和 ownProps
merge 之后才會被賦給 MyComp
。connect
的第三個參數就是用來做這件事。通常情況下,你可以不傳這個參數,connect
就會使用 Object.assign
替代該方法。
3.4. 其他
最后還有一個 options
選項,比較簡單,基本上也不大會用到(尤其是你遵循了其他的一些 React 的「最佳實踐」的時候),本文就略過了。希望了解的同學可以直接看文檔。
(完)
4. 附:connect 方法的官方英文文檔
4.0.1. connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
Connects a React component to a Redux store.
It does not modify the component class passed to it. Instead, it returns a new, connected component class, for you to use.
4.0.2. Arguments
- [mapStateToProps(state, [ownProps]): stateProps] (Function): If specified, the component will subscribe to Redux store updates. Any time it updates, mapStateToProps will be called. Its result must be a plain object*, and it will be merged into the component’s props. If you omit it, the component will not be subscribed to the Redux store. If ownProps is specified as a second argument, its value will be the props passed to your component, and mapStateToProps will be additionally re-invoked whenever the component receives new props (e.g. if props received from a parent component have shallowly changed, and you use the ownProps argument, mapStateToProps is re-evaluated).
- [mapDispatchToProps(dispatch, [ownProps]): dispatchProps] (Object or Function): If an object is passed, each function inside it will be assumed to be a Redux action creator. An object with the same function names, but with every action creator wrapped into a dispatch call so they may be invoked directly, will be merged into the component’s props. If a function is passed, it will be given dispatch. It’s up to you to return an object that somehow uses dispatch to bind action creators in your own way. (Tip: you may use the bindActionCreators() helper from Redux.) If you omit it, the default implementation just injects dispatch into your component’s props. If ownProps is specified as a second argument, its value will be the props passed to your component, and mapDispatchToProps will be re-invoked whenever the component receives new props.
- [mergeProps(stateProps, dispatchProps, ownProps): props] (Function): If specified, it is passed the result of mapStateToProps(), mapDispatchToProps(), and the parent props. The plain object you return from it will be passed as props to the wrapped component. You may specify this function to select a slice of the state based on props, or to bind action creators to a particular variable from props. If you omit it, Object.assign({}, ownProps, stateProps, dispatchProps) is used by default.
- [options] (Object) If specified, further customizes the behavior of the connector.
- [pure = true] (Boolean): If true, implements shouldComponentUpdate and shallowly compares the result of mergeProps, preventing unnecessary updates, assuming that the component is a “pure” component and does not rely on any input or state other than its props and the selected Redux store’s state. Defaults to true.
- [withRef = false] (Boolean): If true, stores a ref to the wrapped component instance and makes it available via getWrappedInstance() method. Defaults to false.