#javascript #reactjs #redux #react-redux #mern
#javascript #reactjs #redux #реагировать-redux #mern
Вопрос:
Я использую MERN и Redux.
У меня есть функция clickHandler, которая вызывает функцию findAuthor, которая импортирована из моих действий. Это находит пользователя по его идентификатору и возвращает его. Я добавил пользователя в глобальное состояние. Затем я хочу извлечь пользователя и добавить его имя в локальное состояние, но я не могу заставить это работать. Я продолжаю получать эту ошибку TypeError: this.props.subAuthor не определен. Чего мне здесь не хватает? Когда я пытаюсь просто напечатать на консоли, я не получаю объект, отображаемый до второго щелчка. Как мне заставить его обновиться сразу?
import React, { Component } from "react";
import PropTypes from "prop-types";
import Goo&leSearch from "./Goo&leSearch";
import { connect } from "react-redux";
import { fetchSubjects } from "../../actions/subject";
import { fetchComments } from "../../actions/comment";
import { updateSubject } from "../../actions/subject";
import { &etUser } from "../../actions/authActions";
class Subject extends Component {
// on loadin& 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) =&&t; {
// 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.len&th; i ) {
if (this.props.comments[i].subject === id) {
temp.unshift(this.props.comments[i]);
}
}
this.setState({
comments: temp,
});
// save the subject id to local stora&e
// 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
localStora&e.setItem("passedSubject", id);
//testin& &etUser
this.findAuthor(id); // this updates the tempUser in state
this.setState({ name: this.props.subAuthor.name });
};
// hoverin& on and off subjects to&&les the visibility of the summary
hoverHandler = (id) =&&t; {
this.setState({ viewSummary: id });
};
hoverOffHandler = () =&&t; {
this.setState({ viewSummary: -1 });
};
rateHandler = (id, rate) =&&t; {
const subject = this.props.subjects.find((subject) =&&t; subject._id === id);
// when no subject was found, the updateSubject won't be called
subject amp;amp;
this.props.updateSubject(id, rate, subject.noOfVotes, subject.ratin&);
alert("Thank you for ratin& this subject.");
};
// take in the id of the subject
// find it in the props
// &et its author id
// call the &etUser passin& the author id
findAuthor(id) {
console.lo&("Hittin& findAuthor function");
const subject = this.props.subjects.find((subject) =&&t; subject._id === id);
const authorId = subject.author;
console.lo&(authorId);
this.props.&etUser(authorId);
}
render() {
const subjectItems = this.props.subjects.map((subject) =&&t; {
// 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}&&t;
<div className="subjectTitle"&&t;
<p
className="title"
onClick={() =&&t; this.clickHandler(subject._id)}
onMouseEnter={() =&&t; this.hoverHandler(subject._id)}
onMouseLeave={() =&&t; this.hoverOffHandler()}
&&t;
{subject.title}
</p&&t;
<p className="rate"&&t;
Rate this subject:
<button onClick={() =&&t; this.rateHandler(subject._id, 1)}&&t;
1
</button&&t;
<button onClick={() =&&t; this.rateHandler(subject._id, 2)}&&t;
2
</button&&t;
<button onClick={() =&&t; this.rateHandler(subject._id, 3)}&&t;
3
</button&&t;
<button onClick={() =&&t; this.rateHandler(subject._id, 4)}&&t;
4
</button&&t;
<button onClick={() =&&t; this.rateHandler(subject._id, 5)}&&t;
5
</button&&t;
</p&&t;
<p className="ratin&"&&t;
Ratin&: {(subject.ratin& / subject.noOfVotes).toFixed(1)}/5
</p&&t;
<p className="summary" style={{ display: hover }}&&t;
{subject.summary}
</p&&t;
</div&&t;
<div className="subjectBody " style={{ display: view }}&&t;
<div className="subjectAuthor"&&t;
<p className="author"&&t;
Subject created by: {subjectAuthor}
<br /&&t; {subject.date}
</p&&t;
</div&&t;
<div className="subjectDescription"&&t;
<p className="description"&&t;{subject.description}</p&&t;
</div&&t;
<div className="subjectLinks"&&t;Links:</div&&t;
<div className="subjectComments"&&t;
<p style={{ fontWei&ht: "bold" }}&&t;Comments:</p&&t;
{comments.map((comment, i) =&&t; {
return (
<div key={i} className="sin&leComment"&&t;
<p&&t;
{comment.title}
<br /&&t;
{comment.comment}
<br /&&t;
Comment by : {comment.author}
</p&&t;
</div&&t;
);
})}
<a href="/addcomment"&&t;
<div className="buttonAddComment"&&t;ADD COMMENT</div&&t;
</a&&t;
</div&&t;
</div&&t;
</div&&t;
);
});
return (
<div id="Subject"&&t;
<Goo&leSearch /&&t;
{subjectItems}
</div&&t;
);
}
}
Subject.propTypes = {
fetchSubjects: PropTypes.func.isRequired,
fetchComments: PropTypes.func.isRequired,
updateSubject: PropTypes.func.isRequired,
&etUser: PropTypes.func.isRequired,
subjects: PropTypes.array.isRequired,
comments: PropTypes.array.isRequired,
newPost: PropTypes.object,
subAuthor: PropTypes.object,
};
const mapStateToProps = (state) =&&t; ({
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
&etUser, // used for &ettin& author name
})(Subject, Comment);
Комментарии:
1. Вы находите пользователя в
findAuthor
, а затем обновляетеredux
. Это действие полностью асинхронное. ИclickHandler
то, что вы пытаетесь получить реквизиты сразу послеfindAuthor
вызова, не гарантирует, что реквизиты будут обновлены новыми данными. Вам нужно будет найти другой способ установить состояние после нахождения автора2. Есть предложения?
3. Поскольку вы уже используете
componentWillReceiveProps
, попробуйте обновить состояние, еслиprevState
имя не совпадает с реквизитами.4. Как бы мне это сделать? Извините, я новичок в этом и не до конца понимаю все это.
5. Интересно, нужно ли вам это в вашем глобальном состоянии, имеет ли это значение? Похоже, это может быть частью состояния локального компонента, или это должно быть частью
fetchComments
, или частью компонента, который отображает комментарий или самого автора, если вы добавите его в глобальное состояние, что потенциально может быть проблематичным в долгосрочной перспективе
Ответ №1:
Я хотел бы предложить альтернативное решение текущему коду, который вы писали до сих пор. Я знаю, что это не codereview (и это было бы не по теме, если только это не действительно рабочий код), но все же я хотел бы показать вам другой способ разделения ваших компонентов.
Из того, что я вижу, у вас много компонентов, в настоящее время все они упакованы в один очень большой компонент. Это может усложнить ситуацию в долгосрочной перспективе, и если вы можете, вам следует избегать этого.
Как я вижу из опубликованного вами кода, у вас действительно есть несколько компонентов, которые я разделил на:
- Тема
- Комментарий
- Пользователь
- Оценка
- Ratin&Viewer
Разделяя ваш теперь большой компонент, вы упрощаете обработку данных для одного компонента позже и повторное использование создаваемых вами компонентов. Возможно, вы захотите повторно использовать некоторые из этих компонентов.
В качестве альтернативного решения я создал очень быструю и базовую демонстрацию того, как вы могли бы провести рефакторинг своего кода. Это всего лишь предложение в надежде, что оно также решит вашу текущую проблему.
Проблема, с которой вы сталкиваетесь, заключается в том, что вы хотите загрузить эти данные и использовать их напрямую. Однако любая операция выборки является асинхронной, поэтому после вашего вызова this.props.&etUser(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: 'Interestin& book' },
{ id: 2, subject: 2, user: 3, comment: 'Is interestin& the only word you know, you twit' }
],
subjects: [
{
id: 1,
title: 'Some interestin& title',
summary: 'Some interestin& summary / plot point',
author: 2,
rate: 0,
noOfVotes: 0
},
{
id: 2,
title: 'Some less interestin& title',
summary: 'Some more interestin& 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 ) =&&t; {
switch (action.type) {
case 'author/add':
return { ...state, [action.payload.id]: action.payload };
default:
return state;
}
};
const userReducer = ( state = {}, action ) =&&t; {
switch (action.type) {
case 'user/add':
return { ...state, [action.payload.id]: action.payload };
default:
return state;
}
};
const subjectReducer = ( state = {}, action ) =&&t; {
switch (action.type) {
case 'subject/retrieved':
return Object.assi&n( {}, ...action.payload.map( subject =&&t; ({ [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 ) =&&t; {
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) =&&t; {
const entities = db[entity];
return Promise.resolve( (filter ? entities.filter( filter ) : entities).map( e =&&t; ({...e}) ) );
}
const fakeUpdate = (entity, id, updatedValue ) =&&t; {
const tar&etEntity = db[entity].find( e =&&t; e.id === id );
if (!tar&etEntity) {
return Promise.reject();
}
Object.assi&n( tar&etEntity, updatedValue );
return Promise.resolve( { ...tar&etEntity } );
}
// separate components
class App extends Component {
render() {
return <Subjects /&&t;;
}
}
// subjects component
// cares about retrievin& the subjects and displayin& them
class SubjectsComponent extends Component {
componentDidMount() {
this.props.fetchSubjects();
}
render() {
const { subjects } = this.props;
if (!subjects || !subjects.len&th) {
return <div&&t;Loadin&</div&&t;;
}
return (
<div&&t;
{ subjects.map( subject =&&t; <Subject key={subject.id} subject={subject} /&&t; ) }
</div&&t;
);
}
}
// 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 chan&ed aswell
// to be aware of that
class SubjectComponent extends Component {
componentDidMount() {
this.props.fetchComments();
}
render() {
const { subject } = this.props;
return (
<div className="subject"&&t;
<h1&&t;{ subject.title }<RateView subject={subject} /&&t;</h1&&t;
<p&&t;{ subject.summary }</p&&t;
<Rate subject={subject} /&&t;
<h2&&t;Comments</h2&&t;
{ this.props.comments amp;amp; this.props.comments.map( comment =&&t; <Comment key={comment.id} comment={comment} /&&t; ) }
</div&&t;
);
}
}
// Just displays a comment and a User component
const Comment = ({ comment }) =&&t; {
return (
<div className="comment"&&t;
<p&&t;{ comment.comment }</p&&t;
<User id={comment.user} /&&t;
</div&&t;
);
}
// 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"&&t;{ this.props.user amp;amp; this.props.user.name }</span&&t;;
}
}
// shows the current ratin& of a post
const RateView = ({ subject }) =&&t; {
if (subject.noOfVotes === 0) {
return <span className="ratin&"&&t;No ratin& yet</span&&t;;
}
const { rate, noOfVotes } = subject;
return <span className="ratin&"&&t;Total ratin& { (rate / noOfVotes).toFixed(1) }</span&&t;;
}
// enables votin& on a subject, can be tri&&ered once per renderin&
// 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.tar&et.&etAttribute('data-value') );
const { subject } = this.props;
this.setState({ hasRated: true, rateValue: userRate }, () =&&t; {
this.props.updateRate( { ...subject, rate: subject.rate userRate, noOfVotes: subject.noOfVotes 1 } );
});
}
render() {
if (this.state.hasRated) {
return <span className="user-rate"&&t;You rated this subject with { this.state.rateValue }</span&&t;;
}
return (
<div&&t;
{ [1, 2, 3, 4, 5].map( value =&&t; <button type="button" onClick={ this.onRateClicked } data-value={value} key={value}&&t;{ value }</button&&t; ) }
</div&&t;
);
}
}
// connectin& all the components to the store, with their states and dispatchers
const Subjects = connect(
state =&&t; ({ subjects: Object.values( state.subjects ) }),
dispatch =&&t; ({
fetchSubjects() {
return fakeFetch('subjects').then( result =&&t; dispatch({ type: 'subject/retrieved', payload: result }) );
}
}))( SubjectsComponent );
// ownProps will be used to filter only the data required for the component that it is usin&
const Subject = connect(
(state, ownProps) =&&t; ({ comments: state.comments.filter( comment =&&t; comment.subject === ownProps.subject.id ) }),
dispatch =&&t; ({
fetchComments() {
return fakeFetch('comments' ).then( result =&&t; dispatch({ type: 'comment/retrieved', payload: result }) );
}
}))( SubjectComponent );
const User = connect(
(state, ownProps) =&&t; ({ user: state.users[ownProps.id] }),
dispatch =&&t; ({
fetchUser( id ) {
return fakeFetch('users', user =&&t; user.id === id).then( result =&&t; dispatch({ type: 'user/add', payload: result[0] }) );
}
}))( UserComponent );
const Rate = connect( null, dispatch =&&t; ({
updateRate( updatedSubject ) {
return fakeUpdate('subjects', updatedSubject.id, updatedSubject).then( updated =&&t; dispatch({ type: 'subject/update', payload: updated }) );
}
}))( RateComponent );
// bind it all toðer and run the app
const tar&etElement = document.querySelector('#container');
render( <Provider store={store}&&t;<App /&&t;</Provider&&t;, tar&etElement );
.user {
font-style: italic;
font-size: .9em;
}
.comment {
paddin&-left: 10px;
back&round-color: #efefef;
border-top: solid #ddd 1px;
}
h1, h2 {
font-size: .8em;
line-hei&ht: .9em;
}
.ratin& {
paddin&: 5px;
display: inline-block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js" inte&rity="sha512-SUJujhtUWZUlwsABaZNnTFRlvCu7XGBZBL1VF33qRvv&Nk3pBS9E353kca&4JAv05/nsB9sanSXFbdHAUW9 l&==" crossori&in="anonymous"&&t;</script&&t;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js" inte&rity="sha512-SYsXmAblZhruCNUVmTp5/v2a1Fnoia06iJh3 L9B9wUaqpRVjcNBQsqA&lQG9b5 IaVCfLDH5 vW923JL5epZA==" crossori&in="anonymous"&&t;</script&&t;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.1/react-redux.min.js" inte&rity="sha512-Ae6lzX7eAwqencnyfCtoAf2h3tQhsV5DrHiqExqyjKrxvT&PHwwOlM694naWdO2ChMmBk3by5oM2c3soVPbI5&==" crossori&in="anonymous"&&t;</script&&t;
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js" inte&rity="sha512-P36ourTueX/PrXrD4Auc1kVLoTE7bkWrIrkaM0IG2X3Fd90LF&TRo&pZzNBssay0XOXhrI&udf4wFeftdsPDiQ==" crossori&in="anonymous"&&t;</script&&t;
<div id="container"&&t;</div&&t;