【问题标题】:How to Divide React Components into Presentational and Container Components如何将 React 组件划分为展示组件和容器组件
【发布时间】:2018-08-20 12:13:17
【问题描述】:

react 和 redux 还是新手,我一直在开发 MERN 用户注册应用程序,我现在正在工作。

在 redux 文档中,我发现创建者建议在将 redux 与 react 集成时将其代码分成两种类型的组件:Presentational(关注事物的外观)和 Container(关注事物的工作方式)。见https://redux.js.org/basics/usagewithreact

我认为这样可以更好地管理应用程序并提高可扩展性。

对于不熟悉的人,这里对优点进行了很好的解释:https://www.youtube.com/watch?v=NazjKgJp7sQ

我只是在以这种方式掌握概念和重写代码方面挣扎。

这是我编写的用于显示用户创建的 cmets 的帖子组件的示例。它在作为道具传递的更高级别组件中接收来自帖子的数据。作为回报,我将所有标记都应用了引导样式。我正在订阅我通过创建事件处理程序导入和使用的 redux 操作。

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import classnames from 'classnames';
import { Link } from 'react-router-dom';
import { deletePost, addLike, removeLike } from '../../actions/postActions';

class PostItem extends Component {
  onDeleteClick(id) {
    this.props.deletePost(id);
  }

  onLikeClick(id) {
    this.props.addLike(id);
  }

  onUnlikeClick(id) {
    this.props.removeLike(id);
  }

  findUserLike(likes) {
    const { auth } = this.props;
    if (likes.filter(like => like.user === auth.user.id).length > 0) {
      return true;
    } else {
      return false;
    }
  }

  render() {
    const { post, auth, showActions } = this.props;

    return (
      <div className="card card-body mb-3">
        <div className="row">
          <div className="col-md-2">
            <a href="profile.html">
              <img
                className="rounded-circle d-none d-md-block"
                src={post.avatar}
                alt=""
              />
            </a>
            <br />
            <p className="text-center">{post.name}</p>
          </div>
          <div className="col-md-10">
            <p className="lead">{post.text}</p>
            {showActions ? (
              <span>
                <button
                  onClick={this.onLikeClick.bind(this, post._id)}
                  type="button"
                  className="btn btn-light mr-1"
                >
                  <i
                    className={classnames('fas fa-thumbs-up', {
                      'text-info': this.findUserLike(post.likes)
                    })}
                  />
                  <span className="badge badge-light">{post.likes.length}</span>
                </button>
                <button
                  onClick={this.onUnlikeClick.bind(this, post._id)}
                  type="button"
                  className="btn btn-light mr-1"
                >
                  <i className="text-secondary fas fa-thumbs-down" />
                </button>
                <Link to={`/post/${post._id}`} className="btn btn-info mr-1">
                  Comments
                </Link>
                {post.user === auth.user.id ? (
                  <button
                    onClick={this.onDeleteClick.bind(this, post._id)}
                    type="button"
                    className="btn btn-danger mr-1"
                  >
                    <i className="fas fa-times" />
                  </button>
                ) : null}
              </span>
            ) : null}
          </div>
        </div>
      </div>
    );
  }
}

PostItem.defaultProps = {
  showActions: true,
};

PostItem.propTypes = {
  deletePost: PropTypes.func.isRequired,
  addLike: PropTypes.func.isRequired,
  removeLike: PropTypes.func.isRequired,
  post: PropTypes.object.isRequired,
  auth: PropTypes.object.isRequired,
};

const mapStateToProps = state => ({
  auth: state.auth,
});

export default connect(mapStateToProps, { deletePost, addLike, removeLike })(PostItem);

如您所见,代码不像我希望的那样简洁紧凑。我的目标是让展示组件不知道 redux,并在此处进行所有样式设置和引导工作,而容器组件具有 redux 和连接功能。有谁知道我应该如何处理这个问题?

我看到人们使用 connect 将这些类型的组件链接在一起:

const PostItemContainer = connect(
    mapStateToProps, 
    { deletePost, addLike, removeLike }
)(PostItem);

export default PostItemContainer;

但我不知道如何在实践中实现这一点。 如果您能帮我解释并提供一些示例代码,那将是非常棒的。

提前致谢!

【问题讨论】:

  • @jsDevia 的回答很好,虽然我不会为小型应用程序使用onClick 的箭头功能,但我认为这对小型应用程序来说不是问题。我想知道,你的Post 组件连接到 Redux 了吗?
  • 是的,我的 Post 组件也连接到了 redux。您是否建议只将最高级别的组件连接到 redux?
  • 如果这个组件可以是所有其他展示组件的容器组件,是的,我可以建议。 @jsDevia 的回答与此逻辑类似,但他为此使用了另一个组件。但是,您已经有一个 Post 组件,并且它已连接到 Redux。

标签: javascript reactjs react-redux components mern


【解决方案1】:

我总是将我的 html 之类(演示文稿)代码放在另一个文件中,作为反应,他们称之为无状态组件,

关键组件是PostItemComponent,它对redux一无所知。

请看下面的代码:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import classnames from 'classnames';
import { Link } from 'react-router-dom';
import { deletePost, addLike, removeLike } from '../../actions/postActions';

const PostItemComponent = ({
    post,
    showActions,
    auth,
    onLikeClick,
    findUserLike,
    onUnlikeClick,
    onDeleteClick
}) => {
    return (
        <div className="card card-body mb-3">
            <div className="row">
                <div className="col-md-2">
                    <a href="profile.html">
                        <img
                            className="rounded-circle d-none d-md-block"
                            src={post.avatar}
                            alt=""
                        />
                    </a>
                    <br />
                    <p className="text-center">{post.name}</p>
                </div>
                <div className="col-md-10">
                    <p className="lead">{post.text}</p>
                    {showActions ? (
                        <span>
                            <button
                                onClick={(event) => onLikeClick(event, post._id)}
                                type="button"
                                className="btn btn-light mr-1">
                                <i
                                    className={classnames('fas fa-thumbs-up', {
                                        'text-info': findUserLike(post.likes)
                                    })}
                                />
                                <span className="badge badge-light">{post.likes.length}</span>
                            </button>
                            <button
                                onClick={(event) => onUnlikeClick(event, post._id)}
                                type="button"
                                className="btn btn-light mr-1"
                            >
                                <i className="text-secondary fas fa-thumbs-down" />
                            </button>
                            <Link to={`/post/${post._id}`} className="btn btn-info mr-1">
                                Comments
                            </Link>
                            {post.user === auth.user.id ? (
                                <button
                                    onClick={(event) => onDeleteClick(event, post._id)}
                                    type="button"
                                    className="btn btn-danger mr-1"
                                >
                                    <i className="fas fa-times" />
                                </button>
                            ) : null}
                        </span>
                    ) : null}
                </div>
            </div>
        </div>
    );
};

class PostItem extends Component {
    constructor(props) {
        super(props);
        this.onDeleteClick = this.onDeleteClick.bind(this);
        this.onLikeClick = this.onLikeClick.bind(this);
        this.onUnlikeClick = this.onUnlikeClick.bind(this);
        this.findUserLike = this.findUserLike.bind(this);
    }
    onDeleteClick(event, id) {
        event.preventDefault();
        this.props.deletePost(id);
    }

    onLikeClick(event, id) {
        event.preventDefault();
        this.props.addLike(id);
    }

    onUnlikeClick(event, id) {
        event.preventDefault();
        this.props.removeLike(id);
    }

    findUserLike(likes) {
        const { auth } = this.props;
        if (likes.filter(like => like.user === auth.user.id).length > 0) {
            return true;
        } else {
            return false;
        }
    }

    render() {
        const { post, auth, showActions } = this.props;
        return (
            <PostItemComponent
                post={post}
                auth={auth}
                showActions={showActions}
                onDeleteClick={this.onDeleteClick}
                onLikeClick={this.onLikeClick}
                onUnlikeClick={this.onUnlikeClick}
            />
        );
    }
}

PostItem.defaultProps = {
    showActions: true,
};

PostItem.propTypes = {
    deletePost: PropTypes.func.isRequired,
    addLike: PropTypes.func.isRequired,
    removeLike: PropTypes.func.isRequired,
    post: PropTypes.object.isRequired,
    auth: PropTypes.object.isRequired,
};

const mapStateToProps = state => ({
    auth: state.auth,
});

export default connect(mapStateToProps, { deletePost, addLike, removeLike })(PostItem);

【讨论】:

  • 感谢您的回答!所以基本上你将展示组件嵌套在容器组件中并传递道具。这是如何使用它的最佳实践吗?在文档 (redux.js.org/basics/usagewithreact) 中,我看到了连接和 mapDispatchToProps 的不同方法。这究竟是如何工作的?
  • 是的,这就是我的解决方案,这样您就可以以适当的方式管理不同的逻辑。
  • 我一直在按照您建议的格式更改我的所有组件,并且效果很好。非常感谢!是否有理由仅在容器组件中使用道具类型?根据这篇文章 (medium.com/@learnreact/container-components-c0e67432e005),prop-types 在组件中进行了验证,因此如果出现问题,组件可能会大声“失败”。这究竟是如何工作的,我应该只在容器中使用 prop-types 还是两个文件都使用?
【解决方案2】:

这与@jsDevia 的答案非常相似,但我没有在这里创建单独的组件,因为您说您的Post 组件已经连接到Redux。因此,您可以获取所有动作创建者并在那里声明并将它们传递给您的 PostItem 组件。

第二个区别是我使用功能组件而不是类组件,因为这里不需要任何状态或生命周期方法。

第三个区别很小。我从您的 onClick 处理程序中删除了所有绑定。对于this 范围问题,我正在为处理程序使用箭头函数。同样,我们不需要任何参数,例如 post._id 来传递这些函数,因为我们已经有 post 作为这里的道具。这就是分离我们的组件的美妙之处。

在回调处理程序中使用 bind 或箭头函数会导致大型应用程序出现一些性能问题,这些应用程序具有很多组件,例如 Post。由于每次渲染此组件时都会重新创建这些功能。但是,使用函数引用可以防止这种情况发生。

const PostItem = ({
  post,
  deletePost,
  addLike,
  removeLike,
  auth,
  showActions,
}) => {

  const onDeleteClick = () => deletePost(post._id);
  const onLikeClick = () => addLike(post._id);
  const onUnlikeClick = () => removeLike(post._id);
  const findUserLike = likes => {
    if (likes.filter(like => like.user === auth.user.id).length > 0) {
      return true;
    } else {
      return false;
    }
  };

  return (
    <div className="card card-body mb-3">
      <div className="row">
        <div className="col-md-2">
          <a href="profile.html">
            <img
              className="rounded-circle d-none d-md-block"
              src={post.avatar}
              alt=""
            />
          </a>
          <br />
          <p className="text-center">{post.name}</p>
        </div>
        <div className="col-md-10">
          <p className="lead">{post.text}</p>
          {showActions ? (
            <span>
              <button
                onClick={onLikeClick}
                type="button"
                className="btn btn-light mr-1"
              >
                <i
                  className={classnames("fas fa-thumbs-up", {
                    "text-info": findUserLike(post.likes),
                  })}
                />
                <span className="badge badge-light">{post.likes.length}</span>
              </button>
              <button
                onClick={onUnlikeClick}
                type="button"
                className="btn btn-light mr-1"
              >
                <i className="text-secondary fas fa-thumbs-down" />
              </button>
              <Link to={`/post/${post._id}`} className="btn btn-info mr-1">
                Comments
              </Link>
              {post.user === auth.user.id ? (
                <button
                  onClick={onDeleteClick}
                  type="button"
                  className="btn btn-danger mr-1"
                >
                  <i className="fas fa-times" />
                </button>
              ) : null}
            </span>
          ) : null}
        </div>
      </div>
    </div>
  );
};

顺便说一句,不要纠结于 Redux 文档中给出的示例。我认为这对新手来说有点复杂。

【讨论】:

  • 非常感谢,这使它更加干净!此外,我读过使用写为箭头函数的事件处理程序仍然属于实验语法(reactjs.org/docs/handling-events.html)。这会影响性能吗?它确实避免了函数绑定的需要,因此可以编写为没有构造函数的函数组件。我想知道这是一种权衡还是实际上可以安全使用。
  • 箭头函数在转译时比常规函数稍慢。但我不认为这种差异有什么大不了的。顺便说一句,类或功能分离不是那样的。如果您不需要状态或任何生命周期方法,您可以使用并且应该使用功能组件。我们不使用类组件,因为我们必须绑定我们的函数。这不是原因 :) 我们使用类组件,因为我们需要一个状态或生命周期方法,我们在这个组件中绑定我们的函数,因为我们需要在回调中使用this。功能组件没有这些问题。
  • 感谢您的清晰解释。是的,除了更具可读性的代码之外,使其无状态是我想要使用函数式组件的原因之一。这样我就不会在没有状态时意外更改状态。
猜你喜欢
  • 2017-06-13
  • 2017-05-28
  • 2017-02-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-06-11
  • 2017-07-06
  • 2016-12-28
相关资源
最近更新 更多