如何将 React 组件划分为展示组件和容器组件

How to Divide React Components into Presentational and Container Components

对 React 和 redux 还是个新手,一直在开发 MERN 用户注册应用程序,我现在开始工作了。

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

我认为这可以更好地管理和扩展应用程序。

对于不熟悉的人,这里有一个很好的优势解释:https://www.youtube.com/watch?v=NazjKgJp7sQ

我只是纠结于概念的把握和代码的重写。

这是我编写的用于显示用户创建的评论的 post 组件的示例。它正在从更高级别的组件中的 post 接收数据作为道具传递。在 return 中,我所有的标记都应用了 bootstrap 样式。我正在订阅我通过创建事件处理程序导入和使用的 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,并在此处执行所有样式和 bootstrap 操作,而容器组件具有 redux 和连接功能。有谁知道我应该如何处理这个问题?

我看到人们一起使用连接到 link 这些类型的组件:

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

export default PostItemContainer;

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

提前致谢!

我总是将我的 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);

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

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

第三个区别很小。我从您的 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 文档中给出的示例。我认为这对新手来说有点复杂。