redux学习理解06

redux-study

Posted by EWL on December 15, 2018

react-redux的学习实现第二课:使用高阶组件包裹纯函数的方式提高组件复用性

上一节的代码中展示了如何将react和redux结合起来,但是我们每一次都需要组件从context中取到store,然后再进行状态的更新。这样看来,每一次使用的时候我们都需要去获取,并也需要提前去在使用store的一系列子组件的父级组件上设置context,这样紧密的耦合,直接导致了我们组件的复用性降为零。所以这一节中react小书展示了如何去解耦合并实践一下高级组件的使用。

image.png

如上图所示,我们所要做的就是将context的绑定放在高阶组件中,通过高阶组件包裹父组件,然后父组件向下传递props来控制一堆dumb组件即可。 这个高阶组件命名为connect,显然它的功能就是用来连接context以及我们的dumb组件。

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
}

一点思考:

第一点:我们在编写这个高阶组件的时候,要怎么把这个store传进去?

第二点:我们使用高阶组件去包裹的dumb组件他们需要的内容不都是一样的,比如Header组件只需要一个主题色themeColor即可,但是ThemeSwitch除了主题色之外,还需要传入一个切换颜色的控制函数,这样一来我们要如何去区分传入的store的内容?

解决思路,如作者所说,我们可以将下面这样的函数当做参数传给connect函数:

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

然后在connect函数的内部的context里面通过store.getState传值给mapStateToProps,返回一个dumb组件需要的全部内容的对象,然后将该对象传给高阶组件包裹的组件,具体实现如下:

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

export const connect = (mapStateToProps) => (WrappedComponent) => (
  class extends Component {
    constructor(props) {
      super(props);

    }

    static contextTypes = {
      store: PropTypes.object
    };

    render() {
      const { store } = this.context;
      const stateProps = mapStateToProps(store.getState());
      return <WrappedComponent {...stateProps}/>
    }
  }
);

具体的使用方法如下:

//如果是Header组件,他仅需要themeColor
const mapStateToProps = (state) => {
  return {
    themeColor: state.themeColor
  }
}
Header = connect(mapStateToProps)(Header)

//如果是ThemeSwitch,他需要themeColor以及switchColor函数
const mapStateToProps = (state) => {
  return {
    themeColor: state.themeColor,//propTypes限制为字符串类型
	switchColor: state.switchColor,//propTypes限制为函数类型
  }
}
ThemeSwitch = connect(mapStateToProps)(ThemeSwitch)

此时完整的代码如下:

Header组件(保留了原来的部分,注释后做对比)

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

class Header extends Component {
  constructor(props) {
    super(props);
    this.state = {
      themeColor: ''
    };
  }

  static propTypes = {
    // store: PropTypes.object
    themeColor: PropTypes.string
  };

  // componentWillMount() {
  //   const { store } = this.context;
  //   this._updateThemeColor();
  //   store.subscribe(this._updateThemeColor);
  // }

  // _updateThemeColor = () => {
  //   const { store } = this.context;
  //   const state = store.getState();
  //
  //   this.setState({
  //     themeColor: state.themeColor
  //   });
  // };

  render() {
    return (
      <h1 style=>我是Title</h1>
    )
  }
}

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

Content组件

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

class Content extends Component {
  constructor(props) {
    super(props);
    // this.state = {
    //   themeColor: ''
    // };
  }

  static propTypes = {
    // store: PropTypes.object
    themeColor: PropTypes.string
  };

  // componentWillMount() {
  //   const { store } = this.context;
  //   this._updateThemeColor();
  //   store.subscribe(this._updateThemeColor);
  // }
  //
  // _updateThemeColor = () => {
  //   const { store } = this.context;
  //   const state = store.getState();
  //
  //   this.setState({
  //     themeColor: state.themeColor
  //   });
  // };

  render() {
    return (
      <div>
        <p style=>我是Content</p>
        <ThemeSwitch />
      </div>
    )
  }
}
const mapStateToProps = (state) => {
  return {
    themeColor: state.themeColor
  }
};
Content = connect(mapStateToProps)(Content);
export default Content;

但是此时刷新页面之后,点击按钮,只有按钮会变色,标题和内容不变,这是因为我们在改造时并没有考虑到还有dispatch函数去触发订阅函数,现在开始修改这一部分。 首先,回想一下,之前触发dispatch时的订阅是怎么写的。

//旧代码
componentWillMount() {
    const { store } = this.context;
    this._updateThemeColor();
    store.subscribe(this._updateThemeColor);
  }

  _updateThemeColor = () => {
    const { store } = this.context;
    const state = store.getState();

    this.setState({
      themeColor: state.themeColor
    });
  };

每一个组件都是在componentWillMount阶段往事件队列里面推入订阅事件,该订阅事件为更新当前组件的state,那我们就可以把这一部分转移到高阶组件中,让高阶组件帮我们做完这一部分工作。 此时,react-redux文件的代码如下:

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

export const connect = (mapStateToProps) => (WrappedComponent) => (
  class 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;
      console.log('this.props', this.props);
      let stateProps = mapStateToProps(store.getState(), this.props); // 额外传入 props,让获取数据更加灵活方便
      this.setState({
        allProps: { // 整合普通的 props 和从 state 生成的 props
          ...stateProps,
          ...this.props
        }
      }, () => {
        console.log('this.state.allProps', this.state.allProps);
      });

    }

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

我们做了以下修改:

  1. 将mapStateToProps的参数改为了两个,前一个为state,后一个留给之后可能传入的高阶组件的props。
  2. 在高阶组件中componentWillMount阶段去初始化更新state,然后将更新的内容,传给包裹组件,并将更新state的函数加入事件队列中。

初始化: image.png

点击Blue按钮: image.png

可以在控制台看到我们更新后的最新state以及传递给包裹组件的stateProps内容。

本节重点需要理解的内容:

  1. 将mapStateToProps修改两个参数之后,要理解后一个参数this.props的含义
  2. 时刻记得在组件加载的时候去初始化state之后要将订阅的事件推入队列,否则之后一系列的dispatch触发是不能够引起页面变更的