无法更新状态

时间:2020-08-12 14:27:58

标签: javascript reactjs redux react-redux mern

我正在使用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);

1 个答案:

答案 0 :(得分:0)

我想为您到目前为止编写的当前代码提供替代解决方案。我知道这不是codereview(除非它实际上是在工作的代码,否则不会在那里讨论),但是,我仍然想向您展示一种划分组件的不同方法。

据我所见,您有许多组件,目前全部打包成一个非常大的组件。从长远来看,这会使事情复杂化,如果可以的话,应该避免这种情况。

正如我从您发布的代码中看到的那样,您确实有几个组成部分,我将它们划分为:

  • 主题
  • 评论
  • 用户
  • 评分
  • 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>