動手實現 React-redux(三):connect 和 mapStateToProps


我們來觀察一下剛寫下的這幾個組件,可以輕易地發現它們有兩個重大的問題:

  1. 有大量重復的邏輯:它們基本的邏輯都是,取出 context,取出里面的 store,然后用里面的狀態設置自己的狀態,這些代碼邏輯其實都是相同的。
  2. 對 context 依賴性過強:這些組件都要依賴 context 來取數據,使得這個組件復用性基本為零。想一下,如果別人需要用到里面的 ThemeSwitch 組件,但是他們的組件樹並沒有 context 也沒有 store,他們沒法用這個組件了。

對於第一個問題,我們在 高階組件 的章節說過,可以把一些可復用的邏輯放在高階組件當中,高階組件包裝的新組件和原來組件之間通過 props 傳遞信息,減少代碼的重復程度。

對於第二個問題,我們得弄清楚一件事情,到底什么樣的組件才叫復用性強的組件。如果一個組件對外界的依賴過於強,那么這個組件的移植性會很差,就像這些嚴重依賴 context 的組件一樣。

如果一個組件的渲染只依賴於外界傳進去的 props 和自己的 state,而並不依賴於其他的外界的任何數據,也就是說像純函數一樣,給它什么,它就吐出(渲染)什么出來。這種組件的復用性是最強的,別人使用的時候根本不用擔心任何事情,只要看看 PropTypes 它能接受什么參數,然后把參數傳進去控制它就行了。

我們把這種組件叫做 Pure Component,因為它就像純函數一樣,可預測性非常強,對參數(props)以外的數據零依賴,也不產生副作用。這種組件也叫 Dumb Component,因為它們呆呆的,讓它干啥就干啥。寫組件的時候盡量寫 Dumb Component 會提高我們的組件的可復用性。

到這里思路慢慢地變得清晰了,我們需要高階組件幫助我們從 context 取數據,我們也需要寫 Dumb 組件幫助我們提高組件的復用性。所以我們盡量多地寫 Dumb 組件,然后用高階組件把它們包裝一層,高階組件和 context 打交道,把里面數據取出來通過 props 傳給 Dumb 組件。

我們把這個高階組件起名字叫 connect,因為它把 Dumb 組件和 context 連接(connect)起來了:

import React, { Component } from 'react'
import PropTypes from 'prop-types'

export connect = (WrappedComponent) => {
  class Connect extends Component {
    static contextTypes = {
      store: PropTypes.object
    }

    // TODO: 如何從 store 取數據?

    render () {
      return <WrappedComponent />
    }
  }

  return Connect
}

connect 函數接受一個組件 WrappedComponent 作為參數,把這個組件包含在一個新的組件 Connect 里面,Connect 會去 context 里面取出 store。現在要把 store 里面的數據取出來通過 props 傳給 WrappedComponent

但是每個傳進去的組件需要 store 里面的數據都不一樣的,所以除了給高階組件傳入 Dumb 組件以外,還需要告訴高級組件我們需要什么數據,高階組件才能正確地去取數據。為了解決這個問題,我們可以給高階組件傳入類似下面這樣的函數:

const mapStateToProps = (state) => {
  return {
    themeColor: state.themeColor,
    themeName: state.themeName,
    fullName: `${state.firstName} ${state.lastName}`
    ...
  }
}

這個函數會接受 store.getState() 的結果作為參數,然后返回一個對象,這個對象是根據 state 生成的。mapStateTopProps 相當於告知了 Connect 應該如何去 store 里面取數據,然后可以把這個函數的返回結果傳給被包裝的組件:

import React, { Component } from 'react'
import PropTypes from 'prop-types'

export const connect = (mapStateToProps) => (WrappedComponent) => {
  class Connect extends Component {
    static contextTypes = {
      store: PropTypes.object
    }

    render () {
      const { store } = this.context
      let stateProps = mapStateToProps(store.getState())
      // {...stateProps} 意思是把這個對象里面的屬性全部通過 `props` 方式傳遞進去
      return <WrappedComponent {...stateProps} />
    }
  }

  return Connect
}

connect 現在是接受一個參數 mapStateToProps,然后返回一個函數,這個返回的函數才是高階組件。它會接受一個組件作為參數,然后用 Connect 把組件包裝以后再返回。 connect 的用法是:

...
const mapStateToProps = (state) => {
  return {
    themeColor: state.themeColor
  }
}
Header = connect(mapStateToProps)(Header)
...

有些朋友可能會問為什么不直接 const connect = (mapStateToProps, WrappedComponent),而是要額外返回一個函數。這是因為 React-redux 就是這么設計的,而個人觀點認為這是一個 React-redux 設計上的缺陷,這里有機會會在關於函數編程的章節再給大家科普,這里暫時不深究了。

我們把上面 connect 的函數代碼單獨分離到一個模塊當中,在 src/ 目錄下新建一個 react-redux.js,把上面的 connect 函數的代碼復制進去,然后就可以在 src/Header.js 里面使用了:

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from './react-redux'

class Header extends Component {
  static propTypes = {
    themeColor: PropTypes.string
  }

  render () {
    return (
      <h1 style={{ color: this.props.themeColor }}>React.js 小書</h1>
    )
  }
}

const mapStateToProps = (state) => {
  return {
    themeColor: state.themeColor
  }
}
Header = connect(mapStateToProps)(Header)

export default Header

可以看到 Header 刪掉了大部分關於 context 的代碼,它除了 props 什么也不依賴,它是一個 Pure Component,然后通過 connect 取得數據。我們不需要知道 connect 是怎么和 context 打交道的,只要傳一個 mapStateToProps 告訴它應該怎么取數據就可以了。同樣的方式修改 src/Content.js

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ThemeSwitch from './ThemeSwitch'
import { connect } from './react-redux'

class Content extends Component {
  static propTypes = {
    themeColor: PropTypes.string
  }

  render () {
    return (
      <div>
        <p style={{ color: this.props.themeColor }}>React.js 小書內容</p>
        <ThemeSwitch />
      </div>
    )
  }
}

const mapStateToProps = (state) => {
  return {
    themeColor: state.themeColor
  }
}
Content = connect(mapStateToProps)(Content)

export default Content

connect 還沒有監聽數據變化然后重新渲染,所以現在點擊按鈕只有按鈕會變顏色。我們給 connect 的高階組件增加監聽數據變化重新渲染的邏輯,稍微重構一下 connect

export const connect = (mapStateToProps) => (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(store.getState(), this.props) // 額外傳入 props,讓獲取數據更加靈活方便
      this.setState({
        allProps: { // 整合普通的 props 和從 state 生成的 props
          ...stateProps,
          ...this.props
        }
      })
    }

    render () {
      return <WrappedComponent {...this.state.allProps} />
    }
  }

  return Connect
}

我們在 Connect 組件的 constructor 里面初始化了 state.allProps,它是一個對象,用來保存需要傳給被包裝組件的所有的參數。生命周期 componentWillMount 會調用調用 _updateProps 進行初始化,然后通過 store.subscribe 監聽數據變化重新調用 _updateProps

為了讓 connect 返回新組件和被包裝的組件使用參數保持一致,我們會把所有傳給 Connect 的 props 原封不動地傳給 WrappedComponent。所以在 _updateProps 里面會把 stateProps 和 this.props 合並到 this.state.allProps 里面,再通過 render 方法把所有參數都傳給 WrappedComponent

mapStateToProps 也發生點變化,它現在可以接受兩個參數了,我們會把傳給 Connect 組件的 props 參數也傳給它,那么它生成的對象配置性就更強了,我們可以根據 store 里面的 state 和外界傳入的 props 生成我們想傳給被包裝組件的參數。

現在已經很不錯了,Header.js 和 Content.js 的代碼都大大減少了,並且這兩個組件 connect 之前都是 Dumb 組件。接下來會繼續重構 ThemeSwitch


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM