1. 程式人生 > >React-redux: React.js 和 Redux 架構的結合

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