React-redux: React.js 和 Redux 架構的結合

  • 2020 年 3 月 14 日
  • 筆記

通過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 這個高階組件來實現展示組件和容器組件的連接的。