React-redux: React.js 和 Redux 架構的結合
通過Redux 架構理解我們瞭解到 Redux 架構的 store、action、reducers 這些基本概念和工作流程。我們也知道了 Redux 這種架構模式可以和其他的前端庫組合使用,而 React-redux 正是把 Redux 這種架構模式和 React.js 結合起來的一個庫。
Context
在 React 應用中,資料是通過 props 屬性自上而下進行傳遞的。如果我們應用中的有很多元件需要共用同一個資料狀態,可以通過狀態提升的思路,將共同狀態提升到它們的公共父元件上面。但是我們知道這樣做是非常繁瑣的,而且程式碼也是難以維護的。這時會考慮使用 Context,Context 提供了一個無需為每層元件手動新增 props,就能在元件樹間進行資料傳遞的方法。也就是說在一個元件如果設定了 context,那麼它的子元件都可以直接訪問到裡面的內容,而不用通過中間元件逐級傳遞,就像一個全域性變數一樣。
在 App -> Toolbar -> ThemedButton 使用 props 屬性傳遞 theme,Toolbar 作為中間元件將 theme 從 App 元件 傳遞給 ThemedButton 元件。
class App extends React.Component { render() { return <Toolbar theme="dark" />; } } function Toolbar(props) { // Toolbar 元件接受一個額外的“theme”屬性,然後傳遞給 ThemedButton 元件。 // 如果應用中每一個單獨的按鈕都需要知道 theme 的值,這會是件很麻煩的事, // 因為必須將這個值層層傳遞所有元件。 return ( <div> <ThemedButton theme={props.theme} /> </div> ); } class ThemedButton extends React.Component { render() { return <Button theme={this.props.theme} />; } }
使用 context,就可以避免通過中間元素傳遞 props 了
// Context 可以讓我們無須明確地傳遍每一個元件,就能將值深入傳遞進元件樹。 // 為當前的 theme 建立一個 context(“light”為預設值)。 const ThemeContext = React.createContext('light'); class App extends React.Component { render() { // 使用一個 Provider 來將當前的 theme 傳遞給以下的元件樹。 // 無論多深,任何元件都能讀取這個值。 // 在這個例子中,我們將 “dark” 作為當前的值傳遞下去。 return ( <ThemeContext.Provider value="dark"> <Toolbar /> </ThemeContext.Provider> ); } } // 中間的元件再也不必指明往下傳遞 theme 了。 function Toolbar(props) { return ( <div> <ThemedButton /> </div> ); } class ThemedButton extends React.Component { // 指定 contextType 讀取當前的 theme context。 // React 會往上找到最近的 theme Provider,然後使用它的值。 // 在這個例子中,當前的 theme 值為 “dark”。 static contextType = ThemeContext; render() { return <Button theme={this.context} />; } }
雖然解決了狀態傳遞的問題卻引入了 2 個新的問題。
1. 我們引入的 context 就像全域性變數一樣,裡面的資料可以被子元件隨意更改,可能會導致程式不可預測的執行。
2. context 極大地增強了元件之間的耦合性,使得元件的複用性變差,比如 ThemedButton 元件因為依賴了 context 的資料導致複用性變差。
我們知道,redux 不正是提供了管理共享狀態的能力嘛,我們只要通過 redux 來管理 context 就可以啦,第一個問題就可以解決了。
Provider 元件
React-Redux 提供 Provider
元件,利用了 react 的 context 特性,將 store 放在了 context 裡面,使得該元件下面的所有元件都能直接訪問到 store。大致實現如下:
class Provider extends Component { // getChildContext 這個方法就是設定 context 的過程,它返回的物件就是 context,所有的子元件都可以訪問到這個物件 getChildContext() { return { store: this.props.store }; } render() { return this.props.children; } } Provider.childContextTypes = { store: React.PropTypes.object }
那麼我們可以這麼使用,將 Provider 元件作為根元件將我們的應用包裹起來,那麼整個應用的元件都可以訪問到裡面的資料了
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'; import { createStore } from 'redux'; import todoApp from './reducers'; import App from './components/App'; const store = createStore(todoApp); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') )
展示(Dumb Components)元件和容器(Smart Components)元件
還記得我們的第二個問題嗎?元件因為 context 的侵入而變得不可複用。React-Redux 為了解決這個問題,將所有元件分成兩大類:展示元件和容器元件。
展示元件
展示元件有幾個特徵
1. 元件只負責 UI 的展示,沒有任何業務邏輯
2. 元件沒有狀態,即不使用 this.state
3. 元件的資料只由 props 決定
4. 元件不使用任何 Redux 的 API
展示元件就和純函式一樣,返回結果只依賴於它的引數,並且在執行過程裡面沒有副作用,讓人覺得非常的靠譜,可以放心的使用。
import React, { Component } from 'react'; import PropTypes from 'prop-types'; class Title extends Component { static propTypes = { title: PropTypes.string } render () { return ( <h1>{ this.props.title }</h1> ) } }
像這個 Title 元件就是一個展示元件,元件的結果完全由外部傳入的 title 屬性決定。
容器元件
容器元件的特徵則相反
1. 元件負責管理資料和業務邏輯,不負責 UI 展示
2. 元件帶有內部狀態
3. 元件的資料從 Redux state 獲取
4. 使用 Redux 的 API
你可以直接使用 store.subscribe()
來手寫容器元件,但是不建議這麼做,因為這樣無法使用 React-redux 帶來的效能優化。
React-redux 規定,所有的展示元件都由使用者提供,容器元件則是由 React-Redux 的 connect()
自動生成。
高階元件 Connect
React-redux 提供 connect
方法,可以將我們定義的展示元件生成容器元件。connect 函式接受一個展示元件引數,最後會返回另一個容器元件回來。所以 connect 其實是一個高階元件(高階元件就是一個函式,傳給它一個元件,它返回一個新的元件)。
import { connect } from 'react-redux'; import Header from '../components/Header'; export default connect()(Header);
上面程式碼中,Header 就是一個展示元件,經過 connect 處理後變成了容器元件,最後把它匯出成模組。這個容器元件沒有定義任何的業務邏輯,所有不能做任何事情。我們可以通過 mapStateToProps
和 mapDispatchToProps 來定義我們的業務邏輯。
import { connect } from 'react-redux'; import Title from '../components/Title'; const mapStateToProps = (state) => { return { title: state.title } } const mapDispatchToProps = (dispatch) => { return { onChangeColor: (color) => { dispatch({ type: 'CHANGE_COLOR', color }); } } } export default connect(mapStateToProps, mapDispatchToProps)(Title);
mapStateToProps 告訴 connect 我們要取 state 裡的 title 資料,最終 title 資料會以 props 的方式傳入 Title 這個展示元件。
mapStateToProps 還
會訂閱 Store,每當 state 更新的時候,就會自動執行,重新計算展示元件的引數,從而觸發展示元件的重新渲染。
mapDispatchToProps 告訴 connect 我們需要 dispatch action,最終 onChangeColor 會以 props 回撥函式的方式傳入 Title 這個展示元件。
Connect 元件大概的實現如下
export const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => { class Connect extends Component { static contextTypes = { store: PropTypes.object } constructor () { super() this.state = { allProps: {} } } componentWillMount () { const { store } = this.context this._updateProps() store.subscribe(() => this._updateProps()) } _updateProps () { const { store } = this.context let stateProps = mapStateToProps ? mapStateToProps(store.getState(), this.props) // 將 Store 的 state 和容器元件的 state 傳入 mapStateToProps : {} // 判斷 mapStateToProps 是否傳入 let dispatchProps = mapDispatchToProps ? mapDispatchToProps(store.dispatch, this.props) // 將 dispatch 方法和容器元件的 state 傳入 mapDispatchToProps : {} // 判斷 mapDispatchToProps 是否傳入 this.setState({ allProps: { ...stateProps, ...dispatchProps, ...this.props } }) } render () { // 將 state.allProps 展開以容器元件的 props 傳入 return <WrappedComponent {...this.state.allProps} /> } } return Connect }
小結
至此,我們就很清楚了,原來 React-redux 就是通過 Context 結合 Redux 來實現 React 應用的狀態管理,通過 Connect 這個高階元件來實現展示元件和容器元件的連線的。
&n