Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
dwqs opened this issue Sep 19, 2016 · 5 comments
Open

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

dwqs opened this issue Sep 19, 2016 · 5 comments

Comments

@dwqs
Copy link
Owner

dwqs commented Sep 19, 2016

组件拆分

关于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 方法详解

@cike8899
Copy link

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

@dwqs
Copy link
Owner Author

dwqs commented Nov 14, 2016

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

@william-xue
Copy link

真棒 解决了一些疑惑

@Fnine59
Copy link

Fnine59 commented Sep 3, 2018

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

@Flcwl
Copy link

Flcwl commented Jul 6, 2019

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants