dwqs/blog

关于Redux的一些总结(二):组件拆分 & connect

dwqs opened this issue · 5 comments

dwqs commented

组件拆分

关于Redux的一些总结(一):Action & 中间件 & 异步 一文中,有提到可以根据 reducer 对组件进行拆分,而不必将 state 中的数据原封不动地传入组件,可以根据 state 中的数据,动态地输出组件需要的(最小)属性。

在常规的组件开发方式中,组件自身的数据和状态是耦合的,这种方式虽能简化开发流程,在短期内能提高开发效率,但只适用于小型且复杂度不高的SPA 应用开发,而对于复杂的 SPA 应用来说,这种开发方式不具备良好的扩展性。以开发一个评论组件 Comment 为例,常规的开发方式如下:

class CommentList extends Component {
    constructor(){
        super();
        this.state = {commnets: []}
    }

    componentDidMount(){
        $.ajax({
            url:'/my-comments.json',
            dataType:'json',
            success:function(data){
                this.setState({comments:data});
            }.bind(this)
        })
    }

    render(){
        return <ul>{this.state.comments.map(renderComment)}</ul>;
    }

    renderComment({body,author}){
        return <li>{body}-{author}</li>;
    }
}

随着应用的复杂度和组件复杂度的双重增加,现有的组件开发方式已经无法满足需求,它会让组件变得不可控制和难以维护,极大增加后续功能扩展的难度。并且由于组件的状态和数据的高度耦合,这种组件是无法复用的,无法抽离出通用的业务无关性组件,这势必也会增加额外的工作量和开发时间。

在组件的开发过程中,从组件的职责角度上,将组件分为 容器类组件(Container Component)展示类组件(Presentational Component)。前者主要从 state 获取组件需要的(最小)属性,后者主要负责界面渲染和自身的状态(state)控制,为容器组件提供样式。

按照上述的概念,Comment应该有两部分组成:CommentListContainer和CommentList。首先定义一个容器类组件(Container Component):

//CommentListContainer
class CommentListContainer extends Component {
    constructor(){
        super();
        this.state = {commnets: []}
    }

    componentDidMount(){
        $.ajax({
            url:'/my-comments.json',
            dataType:'json',
            success:function(data){
                this.setState({comments:data});
            }.bind(this)
        })
    }

    render(){
        return <CommnetList comments={this.state.comments}/>;
    }
}

容器组件CommentListContainer获取到数据之后,通过props传递给子组件CommentList进行界面渲染。CommentList是一个展示类组件:

//CommentList
class CommentList extends Component {
    constructor(props){
        super(props);
        this.state = {commnets: []}
    }


    render(){
        return <ul>{this.props.comments.map(renderComment)}</ul>;
    }

    renderComment({body,author}){
        return <li>{body}-{author}</li>;
    }
}

将Comment组件拆分后,组件的自身状态和异步数据被分离,界面样式由展示类组件提供。这样,对于后续的业务数据变化需求,只需要更改容器类组件或者增加新的展示类业务组件,极大提高了组件的扩展性。

Container Component

容器类组件主要功能是获取 state 和提供 action,渲染各个子组件。各个子组件或是一个展示类组件,或是一个容器组件,其职责具体如下:

  • 获取 state 数据;
  • 渲染内部的子组件;
  • 无样式;
  • 作为容器,嵌套其它的容器类组件或展示类组件;
  • 为展示类组件提供 action,并提供callback给其子组件。

Presentational Component

展示类组件自身的数据来自于父组件(容器类组件或展示类组件),组件自身提供样式和管理组件状态。展示类组件是状态化的,其主要职责如下:

  • 接受props传递的数据;
  • 接受props传递的callback;
  • 定义style;
  • 使用其它的展示类组件;
  • 可以有自己的状态(state)。

连接器:connect

react-redux 为 React 组件和 Redux 提供的 state 提供了连接。当然可以直接在 React 中使用 Redux:在最外层容器组件中初始化 store,然后将 state 上的属性作为 props 层层传递下去。

class App extends Component{

  componentWillMount(){
    store.subscribe((state)=>this.setState(state))
  }

  render(){

    return <Comp state={this.state}
                 onIncrease={()=>store.dispatch(actions.increase())}
                 onDecrease={()=>store.dispatch(actions.decrease())}/>
  }
}

但这并不是所推荐的方式,相比上述的方式,更好的一个写法是结合 react-redux。

首先在最外层容器中,把所有内容包裹在 Provider 组件中,将之前创建的 store 作为 prop 传给 Provider。

const App = () => {
  return (
    <Provider store={store}>
      <Comp/>
    </Provider>
  )
};

Provider 内的任何一个组件(比如这里的 Comp),如果需要使用 state 中的数据,就必须是「被 connect 过的」组件——使用 connect 方法对「你编写的组件(MyComp)」进行包装后的产物。

class MyComp extends Component {
  // content...
}

const Comp = connect(...args)(MyComp);

connect 会返回一个与 store 连接后的新组件。那么,我们就可以传一个 Presentational Component 给 connect,让 connect 返回一个与 store 连接后的 Container Component。

connect 接受四个参数,返回一个函数:

export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}){
        //code

        return function wrapWithConnect(WrappedComponent){
            //other code
            ....
            //merge props
            function computeMergedProps(stateProps, dispatchProps, parentProps) {
                    const mergedProps = finalMergeProps(stateProps, dispatchProps, parentProps)
                    if (process.env.NODE_ENV !== 'production') {
                           checkStateShape(mergedProps, 'mergeProps')
                    }
                   return mergedProps
             }

                  ....

          render(){

                  //other code
                   ....

                   if (withRef) {
                             this.renderedElement = createElement(
                                     WrappedComponent, {
                                     ...this.mergedProps,
                                     ref: 'wrappedInstance'
                             })
                   } else {
                            this.renderedElement = createElement(
                                    WrappedComponent,
                                     this.mergedProps
                            )
                    }

                    return this.renderedElement
             }

        }
}

wrapWithConnect 接受一个组件作为参数,在 render 会调用 React 的 createElement 基于传入的组件和新的 props 返回一个新的组件。

以 connect 的方式来改写Comment组件:

//CommentListContainer
import getCommentList '../actions/index'
import CommentList '../comment-list.js';

function mapStateToProps(state){
    return {
        comment: state.comment,
        other: state.other
    }
}

function mapDispatchToProps(dispatch) {
    return {
        getCommentList:()=>{ 
            dispatch(getCommentList());
        }
    }
}

export default connect(mapStateToProps,mapDispatchToProps)(CommentList);

在Comment组件中,CommentListContainer 只作为一个连接器作用,连接
CommentList 和 state:

//CommentList
class CommentList extends Component {
    constructor(props){
        super(props);
    }

    componentWillMount(){
        //获取数据
        this.props.getCommentList();
    }

    render(){
        let {comment}  = this.props;

        if(comment.fetching){
            //正在加载
            return <Loading />
        }

        //如果对CommentList item的操作比较复杂,也可以将item作为一个独立组件
        return <ul>{this.props.comments.map(renderComment)}</ul>;
    }

    renderComment({body,author}){
        return <li>{body}-{author}</li>;
    }
}

关于 connect 比较详细的解释可以参考:React 实践心得:react-redux 之 connect 方法详解

容器组件可以嵌套在展示组件中使用吗?该怎么嵌套?

dwqs commented

@cike8899 理论上是可以嵌套的 但是并不推荐 因为会让组件结构变得复杂

真棒 解决了一些疑惑

多谢,解决了我的一些问题

Flcwl commented

使用typescript书写,可以只传单个参数mapDispatcherToProps吗? 如下:(还有什么更优雅的方式)
export const test = connect(null, mapDispatcherToProps)(testComponent);