我正在使用MERN和Redux。
我有一个clickHandler函数,该函数调用从我的操作中导入的findAuthor函数。这将通过其ID查找用户并将其返回。我已将用户添加到全局状态。然后,我想检索用户并将其名称添加到本地状态,但是无法正常工作。我不断收到此错误TypeError:this.props.subAuthor未定义。我在这里想念什么?当我尝试仅打印到控制台时,直到第二次单击我才显示任何对象。我如何立即更新?

import React, { Component } from "react";
import PropTypes from "prop-types";
import GoogleSearch from "./GoogleSearch";
import { connect } from "react-redux";
import { fetchSubjects } from "../../actions/subject";
import { fetchComments } from "../../actions/comment";
import { updateSubject } from "../../actions/subject";
import { getUser } from "../../actions/authActions";

class Subject extends Component {
  // on loading the subjects and comments
  // are fetched from the database
  componentDidMount() {
    this.props.fetchSubjects();
    this.props.fetchComments();
  }

  constructor(props) {
    super(props);
    this.state = {
      // set inital state for subjects
      // description, summary and comments all invisible
      viewDesription: -1,
      viewSummary: -1,
      comments: [],
      name: "",
    };
  }

  componentWillReceiveProps(nextProps) {
    // new subject and comments are added to the top
    // of the arrays
    if (nextProps.newPost) {
      this.props.subjects.unshift(nextProps.newPost);
    }
    if (nextProps.newPost) {
      this.props.comments.unshift(nextProps.newPost);
    }
  }

  clickHandler = (id) => {
    // when a subject title is clicked pass in its id
    // and make the description and comments visible
    const { viewDescription } = this.state;
    this.setState({ viewDescription: viewDescription === id ? -1 : id });
    // add relevant comments to the state
    var i;
    var temp = [];
    for (i = 0; i < this.props.comments.length; i++) {
      if (this.props.comments[i].subject === id) {
        temp.unshift(this.props.comments[i]);
      }
    }
    this.setState({
      comments: temp,
    });
    // save the subject id to local storage
    // this is done incase a new comment is added
    // then the subject associated  with it can be retrieved
    // and added as a property of that comment
    localStorage.setItem("passedSubject", id);
    //testing getUser
    this.findAuthor(id); // this updates the tempUser in state
    this.setState({ name: this.props.subAuthor.name });
  };

  // hovering on and off subjects toggles the visibility of the summary
  hoverHandler = (id) => {
    this.setState({ viewSummary: id });
  };
  hoverOffHandler = () => {
    this.setState({ viewSummary: -1 });
  };

  rateHandler = (id, rate) => {
    const subject = this.props.subjects.find((subject) => subject._id === id);
    // when no subject was found, the updateSubject won't be called
    subject &&
      this.props.updateSubject(id, rate, subject.noOfVotes, subject.rating);
    alert("Thank you for rating this subject.");
  };

  // take in the id of the subject
  // find it in the props
  // get its author id
  // call the getUser passing the author id
  findAuthor(id) {
    console.log("Hitting findAuthor function");
    const subject = this.props.subjects.find((subject) => subject._id === id);
    const authorId = subject.author;
    console.log(authorId);
    this.props.getUser(authorId);
  }

  render() {
    const subjectItems = this.props.subjects.map((subject) => {
      // if the state equals the id set to visible if not set to invisible
      var view = this.state.viewDescription === subject._id ? "" : "none";
      var hover = this.state.viewSummary === subject._id ? "" : "none";
      var comments = this.state.comments;
      var subjectAuthor = this.state.name;
      return (
        <div key={subject._id}>
          <div className="subjectTitle">
            <p
              className="title"
              onClick={() => this.clickHandler(subject._id)}
              onMouseEnter={() => this.hoverHandler(subject._id)}
              onMouseLeave={() => this.hoverOffHandler()}
            >
              {subject.title}
            </p>
            <p className="rate">
              Rate this subject:
              <button onClick={() => this.rateHandler(subject._id, 1)}>
                1
              </button>
              <button onClick={() => this.rateHandler(subject._id, 2)}>
                2
              </button>
              <button onClick={() => this.rateHandler(subject._id, 3)}>
                3
              </button>
              <button onClick={() => this.rateHandler(subject._id, 4)}>
                4
              </button>
              <button onClick={() => this.rateHandler(subject._id, 5)}>
                5
              </button>
            </p>
            <p className="rating">
              Rating: {(subject.rating / subject.noOfVotes).toFixed(1)}/5
            </p>
            <p className="summary" style={{ display: hover }}>
              {subject.summary}
            </p>
          </div>

          <div className="subjectBody " style={{ display: view }}>
            <div className="subjectAuthor">
              <p className="author">
                Subject created by: {subjectAuthor}
                <br /> {subject.date}
              </p>
            </div>

            <div className="subjectDescription">
              <p className="description">{subject.description}</p>
            </div>

            <div className="subjectLinks">Links:</div>

            <div className="subjectComments">
              <p style={{ fontWeight: "bold" }}>Comments:</p>
              {comments.map((comment, i) => {
                return (
                  <div key={i} className="singleComment">
                    <p>
                      {comment.title}
                      <br />
                      {comment.comment}
                      <br />
                      Comment by : {comment.author}
                    </p>
                  </div>
                );
              })}
              <a href="/addcomment">
                <div className="buttonAddComment">ADD COMMENT</div>
              </a>
            </div>
          </div>
        </div>
      );
    });

    return (
      <div id="Subject">
        <GoogleSearch />

        {subjectItems}
      </div>
    );
  }
}

Subject.propTypes = {
  fetchSubjects: PropTypes.func.isRequired,
  fetchComments: PropTypes.func.isRequired,
  updateSubject: PropTypes.func.isRequired,
  getUser: PropTypes.func.isRequired,
  subjects: PropTypes.array.isRequired,
  comments: PropTypes.array.isRequired,
  newPost: PropTypes.object,
  subAuthor: PropTypes.object,
};

const mapStateToProps = (state) => ({
  subjects: state.subjects.items,
  newSubject: state.subjects.item,
  comments: state.comments.items,
  newComment: state.comments.item,
  subAuthor: state.auth.tempUser[0],
});

// export default Subject;
export default connect(mapStateToProps, {
  fetchSubjects,
  fetchComments,
  updateSubject, // rate subject
  getUser, // used for getting author name
})(Subject, Comment);

最佳答案

我想为您到目前为止编写的当前代码提供替代解决方案。我知道这不是codereview(除非它实际上是在工作的代码,否则不会在那里讨论),但是,我仍然想向您展示一种划分组件的不同方法。
从我的 Angular 来看,您有许多组件,目前全部打包成一个非常大的组件。从长远来看,这会使事情复杂化,如果可以的话,应该避免这种情况。
从您发布的代码中可以看到,您确实有几个组件,我将它们分为以下几个部分:

  • 主题
  • 评论
  • 用户
  • 评分
  • RatingViewer

  • 通过分割现在较大的组件,可以使以后处理一个组件的数据变得更容易,并且可以重复使用正在制作的组件。您可能想重用其中的某些组件。
    出于替代解决方案的目的,我就如何重构代码创建了一个非常快速且基本的演示。这只是一个建议,希望它也可以解决您当前的问题。
    您遇到的问题是您想要加载该数据,然后直接使用它。但是,任何获取操作都是异步的,因此,在调用this.props.getUser(authorId);后,您的作者将被添加到您的状态中的某个位置,但是在获取完成并且组件被重新呈现之前,该作者将不可用。
    我希望演示中的信息可以为您提供一些见解,它可能与您的方案不完全匹配,但是它应该为您指明可以做些什么。

    // imports
    const { Component } = React;
    const { Provider, connect } = ReactRedux;
    const { render } = ReactDOM;
    const { createStore, combineReducers } = Redux;
    
    // some fake db data
    const db = {
      comments: [
        { id: 1, subject: 2, user: 2, comment: 'Interesting book' },
        { id: 2, subject: 2, user: 3, comment: 'Is interesting the only word you know, you twit' }
      ],
      subjects: [
        {
          id: 1,
          title: 'Some interesting title',
          summary: 'Some interesting summary / plot point',
          author: 2,
          rate: 0,
          noOfVotes: 0
        },
        {
          id: 2,
          title: 'Some less interesting title',
          summary: 'Some more interesting summary / plot point',
          author: 1,
          rate: 5,
          noOfVotes: 2
        }
      ],
      authors: [
        { id: 1, name: 'John Doe' },
        { id: 2, name: 'Jane Doe' }
      ],
      users: [
        { id: 1, name: 'user 1' },
        { id: 2, name: 'user 2' },
        { id: 3, name: 'user 3' }
      ]
    };
    
    // reducers
    const authorReducer = ( state = {}, action ) => {
      switch (action.type) {
        case 'author/add':
          return { ...state, [action.payload.id]: action.payload };
        default:
          return state;
      }
    };
    
    const userReducer = ( state = {}, action ) => {
      switch (action.type) {
        case 'user/add':
          return { ...state, [action.payload.id]: action.payload };
        default:
          return state;
      }
    };
    
    const subjectReducer = ( state = {}, action ) => {
      switch (action.type) {
        case 'subject/retrieved':
          return Object.assign( {}, ...action.payload.map( subject => ({ [subject.id]: subject }) ) );
        case 'subject/add':
          return { ...state, [action.payload.id]: action.payload };
        case 'subject/update':
          const { id } = action.payload;
          return { ...state, [id]: action.payload };
        default:
          return state;
      }
    };
    
    const commentReducer = ( state = [], action ) => {
      switch (action.type) {
        case 'comment/retrieved':
          return action.payload.slice();
        case 'comments/add':
          return [...state, action.payload ];
        default:
          return state;
      }
    };
    
    // create the store
    const store = createStore( combineReducers({
      users: userReducer,
      authors: authorReducer,
      comments: commentReducer,
      subjects: subjectReducer
    }) );
    
    // some promise aware fetch methods
    const fakeFetch = (entity, filter = null) => {
      const entities = db[entity];
      return Promise.resolve( (filter ? entities.filter( filter ) : entities).map( e => ({...e}) ) );
    }
    
    const fakeUpdate = (entity, id, updatedValue ) => {
      const targetEntity = db[entity].find( e => e.id === id );
      if (!targetEntity) {
        return Promise.reject();
      }
      Object.assign( targetEntity, updatedValue );
      return Promise.resolve( { ...targetEntity } );
    }
    
    // separate components
    class App extends Component {
      render() {
        return <Subjects />;
      }
    }
    
    // subjects component
    // cares about retrieving the subjects and displaying them
    class SubjectsComponent extends Component {
      componentDidMount() {
        this.props.fetchSubjects();
      }
      render() {
        const { subjects } = this.props;
        if (!subjects || !subjects.length) {
          return <div>Loading</div>;
        }
        return (
          <div>
          { subjects.map( subject => <Subject key={subject.id} subject={subject} /> ) }
          </div>
        );
      }
    }
    
    // subject component
    // displays a subject and fetches the comments for "all" subjects
    // this should probably only fetch its own comments, but then reducer has to be changed aswell
    // to be aware of that
    class SubjectComponent extends Component {
      componentDidMount() {
        this.props.fetchComments();
      }
      render() {
        const { subject } = this.props;
        return (
          <div className="subject">
            <h1>{ subject.title }<RateView subject={subject} /></h1>
            <p>{ subject.summary }</p>
            <Rate subject={subject} />
            <h2>Comments</h2>
            { this.props.comments && this.props.comments.map( comment => <Comment key={comment.id} comment={comment} /> ) }
          </div>
        );
      }
    }
    
    // Just displays a comment and a User component
    const Comment = ({ comment }) => {
      return (
        <div className="comment">
          <p>{ comment.comment }</p>
          <User id={comment.user} />
        </div>
      );
    }
    
    // User component
    // fetches the user in case he hasn't been loaded yet
    class UserComponent extends Component {
      componentDidMount() {
        if (!this.props.user) {
          this.props.fetchUser( this.props.id );
        }
      }
      render() {
        return <span className="user">{ this.props.user && this.props.user.name }</span>;
      }
    }
    
    // shows the current rating of a post
    const RateView = ({ subject }) => {
      if (subject.noOfVotes === 0) {
        return <span className="rating">No rating yet</span>;
      }
      const { rate, noOfVotes } = subject;
      return <span className="rating">Total rating { (rate / noOfVotes).toFixed(1) }</span>;
    }
    
    // enables voting on a subject, can be triggered once per rendering
    // this should truly be combined with the user who rated the subject, but it's a demo
    class RateComponent extends Component {
      constructor() {
        super();
        this.onRateClicked = this.onRateClicked.bind( this );
        this.state = {
          hasRated: false,
          rateValue: -1
        };
      }
      onRateClicked( e ) {
        const userRate = parseInt( e.target.getAttribute('data-value') );
        const { subject } = this.props;
        this.setState({ hasRated: true, rateValue: userRate }, () => {
          this.props.updateRate( { ...subject, rate: subject.rate + userRate, noOfVotes: subject.noOfVotes + 1 } );
        });
      }
      render() {
        if (this.state.hasRated) {
          return <span className="user-rate">You rated this subject with { this.state.rateValue }</span>;
        }
        return (
          <div>
          { [1, 2, 3, 4, 5].map( value => <button type="button" onClick={ this.onRateClicked } data-value={value} key={value}>{ value }</button> ) }
          </div>
        );
      }
    }
    
    // connecting all the components to the store, with their states and dispatchers
    const Subjects = connect(
      state => ({ subjects: Object.values( state.subjects ) }),
      dispatch => ({
        fetchSubjects() {
          return fakeFetch('subjects').then( result => dispatch({ type: 'subject/retrieved', payload: result }) );
        }
      }))( SubjectsComponent );
    
    // ownProps will be used to filter only the data required for the component that it is using
    const Subject = connect(
      (state, ownProps) => ({ comments: state.comments.filter( comment => comment.subject === ownProps.subject.id ) }),
      dispatch => ({
        fetchComments() {
          return fakeFetch('comments' ).then( result => dispatch({ type: 'comment/retrieved', payload: result }) );
        }
      }))( SubjectComponent );
    
    const User = connect(
      (state, ownProps) => ({ user: state.users[ownProps.id] }),
      dispatch => ({
        fetchUser( id ) {
          return fakeFetch('users', user => user.id === id).then( result => dispatch({ type: 'user/add', payload: result[0] }) );
        }
      }))( UserComponent );
    
    const Rate = connect( null, dispatch => ({
      updateRate( updatedSubject ) {
        return fakeUpdate('subjects', updatedSubject.id, updatedSubject).then( updated => dispatch({ type: 'subject/update', payload: updated }) );
      }
      }))( RateComponent );
    
    // bind it all together and run the app
    const targetElement = document.querySelector('#container');
    render( <Provider store={store}><App /></Provider>, targetElement );
    .user {
      font-style: italic;
      font-size: .9em;
    }
    .comment {
      padding-left: 10px;
      background-color: #efefef;
      border-top: solid #ddd 1px;
    }
    h1, h2 {
      font-size: .8em;
      line-height: .9em;
    }
    .rating {
      padding: 5px;
      display: inline-block;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js" integrity="sha512-SUJujhtUWZUlwsABaZNnTFRlvCu7XGBZBL1VF33qRvvgNk3pBS9E353kcag4JAv05/nsB9sanSXFbdHAUW9+lg==" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js" integrity="sha512-SYsXmAblZhruCNUVmTp5/v2a1Fnoia06iJh3+L9B9wUaqpRVjcNBQsqAglQG9b5+IaVCfLDH5+vW923JL5epZA==" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.1/react-redux.min.js" integrity="sha512-Ae6lzX7eAwqencnyfCtoAf2h3tQhsV5DrHiqExqyjKrxvTgPHwwOlM694naWdO2ChMmBk3by5oM2c3soVPbI5g==" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js" integrity="sha512-P36ourTueX/PrXrD4Auc1kVLoTE7bkWrIrkaM0IG2X3Fd90LFgTRogpZzNBssay0XOXhrIgudf4wFeftdsPDiQ==" crossorigin="anonymous"></script>
    <div id="container"></div>

    09-30 16:47
    查看更多