React setState when score is 0 - reactjs

I am working on a quiz and now I would like to show different messages to the user depending on what score they have. This code is working but I when the score is 0 no new state is set.
It seems to have something to do with that prevProps.score is only triggered when you have answered something correctly. Is there some other conditional I could use instead maybe?
Below is all code in current state:
class App extends React.Component {
constructor(props) {
super(props);
// Make it somewhat harder for cheaters to inspect the correct answer.
document.getElementById('quiz').setAttribute('data-props', '');
const data = JSON.parse(this.props.quiz);
this.state = {
data: data,
nr: 0,
total: data.length,
showButton: false,
questionAnswered: false,
score: 0,
popUpClass: 'popup-visible',
quizVisible: false,
id: data[0].id,
question: data[0].question,
answers: [
data[0].answers[0],
data[0].answers[1],
data[0].answers[2],
data[0].answers[3]
],
correct: data[0].correct
}
this.nextQuestion = this.nextQuestion.bind(this);
this.handleShowButton = this.handleShowButton.bind(this);
this.handleStartQuiz = this.handleStartQuiz.bind(this);
this.handleIncreaseScore = this.handleIncreaseScore.bind(this);
}
pushData(nr) {
const data = this.state.data;
this.setState({
question: data[nr].question,
answers: [data[nr].answers[0], data[nr].answers[1], data[nr].answers[2], data[nr].answers[3] ],
correct: data[nr].correct,
nr: this.state.nr + 1
});
}
nextQuestion() {
let { nr, total} = this.state;
if(nr === total){
this.setState({
popUpClass: 'popup-visible',
quizVisible: false
});
} else {
this.pushData(nr);
this.setState({
showButton: false,
questionAnswered: false,
quizVisible: true
});
}
}
handleShowButton() {
this.setState({
showButton: true,
questionAnswered: true
});
}
handleStartQuiz() {
this.setState({
popUpClass: 'popup-hidden',
quizVisible: true,
nr: 1
});
}
handleIncreaseScore() {
this.setState({
score: this.state.score + 1
});
}
render() {
let { nr, total, id, question, answers, correct, showButton, questionAnswered, popUpClass, quizVisible, score} = this.state;
return (
<div className="app-container">
<Popup className={popUpClass} score={score} total={total} startQuiz={this.handleStartQuiz} key={nr} />
{quizVisible ?
(
<div key={question} className="quiz">
<div className="quiz-box">
<span className="question-total">Fråga {nr} av {total}</span>
<h2 className="question">{question}</h2>
<Answers
key={id}
answers={answers}
correct={correct}
showButton={this.handleShowButton}
isAnswered={questionAnswered}
increaseScore={this.handleIncreaseScore} />
</div>
<div id="submit">
{showButton ? <button className="fancy-btn" onClick={this.nextQuestion} id={nr === total ? 'finishQuiz' : null}>{nr === total ? 'Slutför quiz' : 'Nästa fråga'}</button> : null}
</div>
</div>
) : null}
</div>
);
}
};
class Popup extends React.Component {
constructor(props) {
super(props);
this.state = {
title: 'Quiz',
showStartButton: true
};
this.startQuizHandle = this.startQuizHandle.bind(this);
}
startQuizHandle() {
this.props.startQuiz();
}
componentDidUpdate(prevProps) {
let total = this.props.total;
let highScore = total - 2;
let halfScore = total / 2;
if (this.props.score !== prevProps.score) {
if (this.props.score >= highScore) {
this.setState({
title: 'You are an expert!',
})
} else if (this.props.score >= halfScore && this.props.score <= highScore) {
this.setState({
title: 'You are pretty good at this!'
})
}
else if (this.props.score < halfScore && this.props.score > 0) {
console.log('score less than half');
this.setState({
title: 'You need some practice.'
})
}
else {
this.setState({
title: 'You did not do too well.',
})
}
}
}
createMarkup(text) {
return {__html: text};
}
render() {
let { title, intro, text, showStartButton } = this.state;
let { className } = this.props;
return (
<div className={`popup-container ${ className }`}>
<div className="popup">
<h1>{title}</h1>
</div>
{showStartButton ? <button className="fancy-btn" onClick={this.startQuizHandle}>Start Quiz</button> : null}
</div>
);
}
}
export default Popup
class Answers extends React.Component {
constructor(props) {
super(props);
this.state = {
isAnswered: false,
classNames: ['', '', ''],
isDisabled: false
}
this.checkAnswer = this.checkAnswer.bind(this);
}
checkAnswer(e) {
let { isAnswered } = this.props;
this.setState({
isDisabled: true
})
if (!isAnswered) {
let elem = e.currentTarget;
let { correct, increaseScore } = this.props;
let answer = Number(elem.dataset.id);
let updatedClassNames = this.state.classNames;
if (answer === correct) {
updatedClassNames[answer - 1] = 'right';
increaseScore();
}
else {
updatedClassNames[answer - 1] = 'wrong';
}
this.setState({
classNames: updatedClassNames
})
this.props.showButton();
}
}
render() {
let { answers } = this.props;
let { classNames } = this.state;
const { isDisabled } = this.state;
return (
<div id="answers">
<ul>
<li onClick={this.checkAnswer} className={classNames[0]} data-id="1"><p className={isDisabled ? "disabled" : null}><span>A</span> {answers[0]}</p></li>
<li onClick={this.checkAnswer} className={classNames[1]} data-id="2"><p className={isDisabled ? "disabled" : null}><span>B</span> {answers[1]}</p></li>
<li onClick={this.checkAnswer} className={classNames[2]} data-id="3"><p className={isDisabled ? "disabled" : null}><span>C</span> {answers[2]}</p></li>
</ul>
</div>
);
}
}
export default Answers

Convert your last else if to an else so your conditional tests are as follows:
if score >= high score => "...expert!"
else if half score <= score < high score => "... petty good"
else if 0 < score < half score => "..need practice"
else => "did not do so well"
This is the "catch-all" branch of logic for scores that didn't fall into one of the previous test cases.
componentDidUpdate
componentDidUpdate(prevProps) {
let total = this.props.total;
let highScore = total - 2;
let halfScore = total / 2;
const { score } = this.props;
if (score !== prevProps.score) {
if (score >= highScore) {
this.setState({
title: "You are an expert!"
});
} else if (score >= halfScore && score <= highScore) {
this.setState({
title: "You are pretty good at this!"
});
} else if (score < halfScore && score > 0) {
this.setState({
title: "You need some practice."
});
} else {
this.setState({
title: "You did not do too well."
});
}
}
}
Edit
Looks like your initial quiz state is to have total be the length of the data array (presumably the quiz questions?) and score starting at 0. From your logic it is clear score monotonically increases from 0. I think the piece that is missing from the Popup component is checking this "initial state" where the score === 0. My guess is you see the title start at "Quiz" and then after the first correct answer it updates to "You need some practice.". A small refactor of the "checking score" logic within componentDidUpdate will allow you to check it when Popup first mounts when the score is 0.
checkScore = () => {
const { score, total } = this.props;
const highScore = total - 2;
const halfScore = total / 2;
if (score >= highScore) {
this.setState({
title: "You are an expert!"
});
} else if (score >= halfScore && score < highScore) {
this.setState({
title: "You are pretty good at this!"
});
} else if (score < halfScore && score > 0) {
this.setState({
title: "You need some practice."
});
} else {
this.setState({
title: "You did not do too well."
});
}
}
componentDidMount() {
this.checkScore();
}
componentDidUpdate(prevProps) {
const { score } = this.props;
if (prevProps.score !== score) {
this.checkScore();
}
}
Updated the above linked codesandbox.

Related

Applying multiple dynamic filters

After now two full days of trial and error and googling, I am starting to lose my mind and really could use some help. This is my second React hobby-project, so please bear with me if it contains any "no-go's".
Situation:
I call an API, store the data in state (hotel rooms), display all rooms at first. After applying a search, I want to narrow it down further - and that works (with hardcoded values for testing).
I take all available rooms, filter them, store them into another array and let that array then get displayed.
The Problem:
My search is not dynamic. I can narrow the results down, but I can't "bring them back up". For example: if a user wants the rooms narrowed down by price and by "pets allowed", it gets displayed. But if he decides that bringing his pet is not that important and unchecks the filter, the results stay the same as before.
The handleClicks and Buttons are just there to quickly test things, they're not how the end result will be. Also, I've left out the fetchRoomsData(), as it's not important here.
If anyone could help me out here, it would be highly appreciated!
Thanks in advance :)
import React, { Component } from "react";
import Roomcard from "./RoomCard.js";
export default class AllRooms extends Component {
constructor() {
super();
this.state = {
data: undefined,
fetched: false,
roomsToDisplay: [],
hasFilter: {
price: 300,
capacity: 3,
pets: true,
breakfast: false,
},
};
}
componentDidMount() {
this.fetchRoomsData();
}
handleClick1() {
this.filterByPrice();
}
handleClick2() {
this.filterByPets();
}
handleClick3() {
this.filterByCapacity();
}
handleClick4() {
this.filterByBreakfast();
}
handleClick5() {
this.generateAllRooms();
}
filterByPrice() {
let tempArr = [];
this.state.roomsToDisplay.map((room) =>
room.props.price < this.state.hasFilter.price ? tempArr.push(room) : null
);
if (tempArr.length > 0) {
this.setState({ roomsToDisplay: tempArr });
} else {
this.setState({
roomsToDisplay: <h1>There are no matching results.</h1>,
});
}
}
filterByPets() {
let tempArr = [];
this.state.roomsToDisplay.map((room) =>
room.props.pets ? tempArr.push(room) : null
);
if (tempArr.length > 0) {
this.setState({ roomsToDisplay: tempArr });
} else {
this.setState({
roomsToDisplay: <h1>There are no matching results.</h1>,
});
}
}
filterByBreakfast() {
let tempArr = [];
this.state.roomsToDisplay.map((room) =>
room.props.breakfast ? tempArr.push(room) : null
);
if (tempArr.length > 0) {
this.setState({ roomsToDisplay: tempArr });
} else {
this.setState({
roomsToDisplay: <h1>There are no matching results.</h1>,
});
}
}
filterByCapacity() {
let tempArr = [];
this.state.roomsToDisplay.map((room) =>
room.props.capacity > this.state.hasFilter.capacity
? tempArr.push(room)
: null
);
if (tempArr.length > 0) {
this.setState({ roomsToDisplay: tempArr });
} else {
this.setState({
roomsToDisplay: <h1>There are no matching results.</h1>,
});
}
}
generateAllRooms() {
let finalDiv = [];
this.state.data.items.map((room) =>
finalDiv.push(
<Roomcard
price={room.fields.price}
titleImage={`https:${room.fields.images[0].fields.file.url}`}
allImages={room.fields.images.map((image) => image.fields.file.url)}
name={room.fields.name.toUpperCase()}
slug={room.fields.slug}
capacity={room.fields.capacity}
type={room.fields.type}
size={room.fields.size}
pets={room.fields.pets}
breakfast={room.fields.breakfast}
featured={room.fields.featured}
description={room.fields.description}
extras={room.fields.extras}
key={Math.random() * 1000}
/>
)
);
this.setState({ roomsToDisplay: finalDiv });
}
render() {
return (
<>
<div className="search-field-outer-box">
<button onClick={() => this.handleClick1()}> Filter By Price </button>
<button onClick={() => this.handleClick2()}> Filter By Pets </button>
<button onClick={() => this.handleClick3()}> Filter By capacity </button>
<button onClick={() => this.handleClick4()}> Filter By breakfast </button>
<button onClick={() => this.handleClick5()}> Reset Filter </button>
</div>
{this.state.data ? (
<div className="room-card-container">{this.state.roomsToDisplay}</div>
) : undefined}
</>
);
}
}
👋 Welcome to SO
First of all, don't store jsx elements in the state, prefer to store only values and create the jsx at render time.
Now, what I would do is to have the whole dataset in a state variable (and never modify it) and another for the filtered data
this.state = {
data:[],
filteredData:[]
};
// Here at some point when retrieving the data,
// this.setState({data: fetchedData, filteredData: fetchedData});
filterByBreakfast() {
const dataFiltered = // your code to filter
this.setState({
filterdData: dataFiltered,
});
}
resetFilters() {
// Reset the Filtered Data
this.setState({
filterdData: this.state.data,
});
}
render() {
return {
<div>
<div>
<button onClick={this.filterByBreakfast}> Filter By breakfast </button>
<button onClick={this.resetFilters}> Reset Filter </button>
</div>
<div>
{filteredData.length > 0 ? filteredData.map(item => <div>{item}</div>) : <div>No results</div>}
</div>
</div>
}
}

What is the best way to update object array value in React

My React state:
//...
this.state = {
mylist: [
{
"id": 0,
"trueorfalse": false
},
{
"id": 1,
"trueorfalse": false
}
]
}
//...
I am trying to update the trueorfalse value based on the id
Here is what I did so far but didn't work:
var idnum = e.target.id.toString().split("_")[1] //getting the id via an element id (0 or 1 in this case)
var TorF = true
if (type === 1) {
this.setState({
mylist: this.state.mylist.map(el => (el.id === idnum ? Object.assign({}, el, { TorF }) : el))
})
}
I really want to make it dynamic so the trueorfase will be opposite of what it is now:
var idnum = e.target.id.toString().split("_")[1] //getting the id via an element id (0 or 1 in this case)
if (type === 1) {
this.setState({
mylist: this.state.mylist.map(el => (el.id === idnum ? Object.assign({}, el, { /* if already true set to false or vice versa */ }) : el))
})
}
How can I update my code to have the dynamicity shown in the second example (if possible), otherwise the first example would do just fine
Another solution using map:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
mylist: [
{
id: 0,
trueorfalse: false
},
{
id: 1,
trueorfalse: true
}
]
};
}
toggleBoolean = () => {
const ID = Number(this.state.selectedID);
this.setState(prevState => ({
mylist: prevState.mylist.map(item => {
if (item.id === ID) {
return { ...item, trueorfalse: !item.trueorfalse };
} else {
return item;
}
})
}));
};
render() {
return (
<div className="App">
<p>{`State values: ${JSON.stringify(this.state.mylist)}`}</p>
<button onClick={this.toggleBoolean}>Change true/false values</button>
<label>Insert ID:</label>
<input
type="number"
onChange={event => this.setState({ selectedID: event.target.value })}
/>
</div>
);
}
}
I think the following code would accomplish your second question.
var idnum = e.target.id.toString().split("_")[1]
let newList = Array.from(this.state.mylist) //create new array so we don't modify state directly
if (type === 1) {
let objToUpdate = newList.find((el) => el.id === idnum) // grab first element with matching id
objToUpdate.trueorfalse = !objToUpdate.trueorfalse
this.setState( { mylist: newList } )
}

Maximum call stack size exceeded;

I meet problem, when i use componentDidMount(), componentDidMount() use to show Tooltip when use function _getContentTooltip() then the problem show the error common.js:444 RangeError: Maximum call stack size exceeded
import ZCommon from 'utils/common';
import React from 'react';
import ReactDOM from 'react-dom';
class TooltipUtil extends React.Component {
constructor(props) {
super(props);
this.state = {
guidesContent: [],
zguides: [],
current: 1,
hidden: 0
};
}
shouldComponentUpdate(nextProps, nextState) {
return (
this.state.current !== nextState.current ||
this.state.zguides !== nextState.zguides
);
}
_storageData() {
this.state.guidesContent = [{
"id":"1",
"description": "Đây là title của người chat or group",
"name":"tabmsg.searchicon",
"title": "abc",
}, {
"id":"2",
"name":"tabmsg.creategroup",
"description": "Bạn click vào đây để tạo nhóm",
"title": "Xưu nhân",
}, {
"id":"3",
"name":"csc.header.search",
"description": "Đây là khung để nhập nội dung chat",
"title": "abc",
}];
this.setState({
guidesContent: this.state.guidesContent.sort(function(a,b){ return a.id > b.id } )
});
return this.state.guidesContent;
}
_getContentTooltip(){
// this.serverRequest.abort();
let current= this.state.current;
let _guides = this._storageData();
let ele = document.querySelectorAll(".tooltip");
for (var i = 0; i < ele.length; i++) {
var no = ele[i].getAttribute('data-tooltip');
let Tcontent = Object.keys(_guides).filter(function(key) {
if(_guides[key].name == no){
if(_guides[key].id == current){
return key;
}
}
});
this.setState({
zguides: this.state.guidesContent[Tcontent]
})
}
}
componentDidMount(){
this._getContentTooltip();
}
componentDidUpdate(){
this.componentDidMount();
}
_handlerClickClose() {
let _guides = this._storageData();
if(this.state.current <= _guides.length ){
this.setState({
current: this.state.current + 1
});
}
}
render() {
let guides = null;
let obj = this.state.zguides;
let show = this.state.zguides != undefined ? "show" : ' ';
console.log(this.state.zguides);
guides = (
<div className={'guide ' + show } style={{ width:'200px',left:'0'}}>
<div className="guide-content flx">
<div href="#" className="fa fa-close" onClick= {this._handlerClickClose.bind(this)}></div>
<h4>{this.state.zguides['title']}</h4>
<p>{this.state.zguides['description']}</p>
</div>
</div>
);
return guides;
}
_handlerClickClose() {
let _guides = this._storageData();
if(this.state.current <= _guides.length ){
this.setState({
current: this.state.current + 1
});
}
}
}
export default TooltipUtil;
In _getContentTooltip() function you are changing the state.which causes the component to update so componentDidUpdate() function runs ,which also calling again componentDidMount(). that function again calls getContentTooltip().so comment the below line
componentDidUpdate(){
//this.componentDidMount();
}
Fist of all you should not force the lifecycle functions to call forcefully. Now the Maximum Stack Size errors occurs because _getContentTooltip is setting state and hence hence will trigger a rerender resulting in componentDidUpdate lifecycle function being called where you again call componentDidMount thus going in an endless loop. You would say that you have checked whether to previous and current state values are equal in shouldComponentUpdate but comparing arrays like this.state.zguides !== nextState.zguides will return true.
See this answer:
How to compare arrays in JavaScript?
If you want to trigger _getContentTooltip periodically, call it in a setInterval function in componentDidMount
class TooltipUtil extends React.Component {
constructor(props) {
super(props);
this.state = {
guidesContent: [],
zguides: [],
current: 1,
hidden: 0
};
}
shouldComponentUpdate(nextProps, nextState) {
return (
this.state.current !== nextState.current ||
!this.state.zguides.equals(nextState.zguides)
);
}
_storageData() {
this.state.guidesContent = [{
"id":"1",
"description": "Đây là title của người chat or group",
"name":"tabmsg.searchicon",
"title": "abc",
}, {
"id":"2",
"name":"tabmsg.creategroup",
"description": "Bạn click vào đây để tạo nhóm",
"title": "Xưu nhân",
}, {
"id":"3",
"name":"csc.header.search",
"description": "Đây là khung để nhập nội dung chat",
"title": "abc",
}];
this.setState({
guidesContent: this.state.guidesContent.sort(function(a,b){ return a.id > b.id } )
});
return this.state.guidesContent;
}
_getContentTooltip(){
// this.serverRequest.abort();
let current= this.state.current;
let _guides = this._storageData();
let ele = document.querySelectorAll(".tooltip");
for (var i = 0; i < ele.length; i++) {
var no = ele[i].getAttribute('data-tooltip');
let Tcontent = Object.keys(_guides).filter(function(key) {
if(_guides[key].name == no){
if(_guides[key].id == current){
return key;
}
}
});
this.setState({
zguides: this.state.guidesContent[Tcontent]
})
}
}
componentDidMount(){
setInterval(() => {
this._getContentTooltip();
}, 1000)
}
_handlerClickClose() {
let _guides = this._storageData();
if(this.state.current <= _guides.length ){
this.setState({
current: this.state.current + 1
});
}
}
render() {
let guides = null;
let obj = this.state.zguides;
let show = this.state.zguides != undefined ? "show" : ' ';
console.log(this.state.zguides);
guides = (
<div className={'guide ' + show } style={{ width:'200px',left:'0'}}>
<div className="guide-content flx">
<div href="#" className="fa fa-close" onClick= {this._handlerClickClose.bind(this)}></div>
<h4>{this.state.zguides['title']}</h4>
<p>{this.state.zguides['description']}</p>
</div>
</div>
);
return guides;
}
_handlerClickClose() {
let _guides = this._storageData();
if(this.state.current <= _guides.length ){
this.setState({
current: this.state.current + 1
});
}
}
}
export default TooltipUtil;

ReactJS seems combine two state updates as one render, how to see separate rendering effects?

I am trying to come up a react exercise for the flip-match cards game: say 12 pairs of cards hide (face down) randomly in a 4x6 matrix, player click one-by-one to reveal the cards, when 2 cards clicked are match then the pair is found, other wise hide both again., gane over when all pairs are found.
let stored = Array(I * J).fill(null).map((e, i) => (i + 1) % (I * J));
/* and: randomize (I * J / 2) pairs position in stored */
class Board extends React.Component {
constructor() {
super();
this.state = {
cards: Array(I*J).fill(null),
nClicked: 0,
preClicked: null,
clicked: null,
};
}
handleClick(i) {
if (!this.state.cards[i]) {
this.setState((prevState) => {
const upCards = prevState.cards.slice();
upCards[i] = stored[i];
return {
cards: upCards,
nClicked: prevState.nClicked + 1,
preClicked: prevState.clicked,
clicked: i,
};
}, this.resetState);
}
}
resetState() {
const preClicked = this.state.preClicked;
const clicked = this.state.clicked;
const isEven = (this.state.nClicked-1) % 2;
const matched = (stored[preClicked] === stored[clicked]);
if (isEven && preClicked && clicked && matched) {
// this.forceUpdate(); /* no effects */
this.setState((prevState) => {
const upCards = prevState.cards.slice();
upCards[preClicked] = null;
upCards[clicked] = null;
return {
cards: upCards,
nClicked: prevState.nClicked,
preClicked: null,
clicked: null,
};
});
}
}
renderCard(i) {
return <Card key={i.toString()} value={this.state.cards[i]} onClick={() => this.handleClick(i)} />;
}
render() {
const status = 'Cards: '+ I + ' x ' + J +', # of clicked: ' + this.state.nClicked;
const cardArray = Array(I).fill(null).map(x => Array(J).fill(null));
return (
<div>
<div className="status">{status}</div>
{ cardArray.map((element_i, index_i) => (
<div key={'row'+index_i.toString()} className="board-row">
{ element_i.map((element_j, index_j) => this.renderCard(index_i*J+index_j))
}
</div>
))
}
</div>
);
}
}
Essentially, Board constructor initialize the state, and handleClick() calls setState() to update the state so it trigger the render of the clicked card's value; the callback function resetState() is that if the revealed two card did not match, then another setState() to hide both.
The problem is, the 2nd clicked card value did not show before it goes to hide. Is this due to React combine the 2 setState renderings in one, or is it rendering so fast that we can not see the first rendering effects before the card goes hide? How to solve this problem?
You're passing resetState as the callback to setState, so I would expect after the initial click your state will be reset.
You might want to simplify a bit and do something like this:
const CARDS = [
{ index: 0, name: 'Card One', matchId: 'match1' },
{ index: 1, name: 'Card Two', matchId: 'match2' },
{ index: 2, name: 'Card Three', matchId: 'match1', },
{ index: 3, name: 'Card Four', 'matchId': 'match2' },
];
class BoardSim extends React.Component {
constructor(props) {
super(props);
this.state = {
cardsInPlay: CARDS,
selectedCards: [],
checkMatch: false,
updateCards: false
};
...
}
...
componentDidUpdate(prevProps, prevState) {
if (!prevState.checkMatch && this.state.checkMatch) {
this.checkMatch();
}
if (!prevState.updateCards && this.state.updateCards) {
setTimeout(() => {
this.mounted && this.updateCards();
}, 1000);
}
}
handleCardClick(card) {
if (this.state.checkMatch) {
return;
}
if (this.state.selectedCards.length === 1) {
this.setState({ checkMatch: true });
}
this.setState({
selectedCards: this.state.selectedCards.concat([card])
});
}
checkMatch() {
if (this.selectedCardsMatch()) {
...
}
else {
...
}
setTimeout(() => {
this.mounted && this.setState({ updateCards: true });
}, 2000);
}
selectedCardsMatch() {
return this.state.selectedCards[0].matchId ===
this.state.selectedCards[1].matchId;
}
updateCards() {
let cardsInPlay = this.state.cardsInPlay;
let [ card1, card2 ] = this.state.selectedCards;
if (this.selectedCardsMatch()) {
cardsInPlay = cardsInPlay.filter((card) => {
return card.id !== card1.id && card.id !== card2.id;
});
}
this.setState({
selectedCards: [],
cardsInPlay,
updateCards: false,
checkMatch: false
});
}
render() {
return (
<div>
{this.renderCards()}
</div>
);
}
renderCards() {
return this.state.cardsInPlay.map((card) => {
return (
<div key={card.name} onClick={() => this.handleCardClick(card)}>
{card.name}
</div>
);
});
}
...
}
I've created a fiddle for this you can check out here: https://jsfiddle.net/andrewgrewell/69z2wepo/82425/

React props are updating automatically

I am making a paginate component, and I'm having problems with the props passed to this paginate component. The elements props are updating automatically, so I cannot use componentWillReceiveProps to update my component.
Here is my parent component render and callback to Paginate's component:
class AlarmManagement extends React.Component{
constructor(props){
super(props)
this.state = {
alarms: undefined,
nameIsOrdered: false,
codeIsOrdered: true,
priorityIsOrdered: false,
alarmsPaginate: undefined,
}
this.orderByName = this.orderByName.bind(this)
this.orderByCode = this.orderByCode.bind(this)
this.orderByPriority = this.orderByPriority.bind(this)
this.getAlarms = this.getAlarms.bind(this)
this.getAlarmsPaginate = this.getAlarmsPaginate.bind(this)
}
componentWillMount(){
this.getAlarms()
}
getAlarms(){
$.ajax({
type: 'GET',
url: '/api/alarm-event-types/',
headers: { 'Authorization': "Token " + localStorage.token },
success: (alarms) => {
this.setState({
alarms: alarms.sort((a, b) => a.code - b.code)
})
},
error: (fail) => console.log(fail)
})
}
getAlarmsPaginate(page, amount) {
const newAlarms = this.state.alarms
this.setState({ alarmsPaginate: newAlarms.slice(page, amount) });
}
componentWillReceiveProps({emergency}){
if(emergency !== undefined && emergency !== null){
let updateList = [],
shouldUpdate = false
emergency.forEach((el, index) => {
if(el.update){
updateList.push(index)
}
if(el.update == "alarm"){
shouldUpdate = true
}
})
if(shouldUpdate){
this.getAlarms()
}
updateList.forEach((el) => {
if(this.props.clearList){
this.props.clearList(el)
}
})
}
}
orderByName(){
let alarms = this.state.alarms
if(this.state.nameIsOrdered === true){
this.setState({
alarms: alarms.sort((a, b) => {
let nameA = `${a.name.toUpperCase()}`,
nameB = `${b.name.toUpperCase()}`
if (nameA > nameB) {
return 1;
}
if (nameA < nameB) {
return -1;
}
return 0;
}),
nameIsOrdered: false,
codeIsOrdered: false,
priorityIsOrdered: false
})
}
else{
this.setState({
alarms: alarms.sort((a, b) => {
let nameA = `${a.name.toUpperCase()}`,
nameB = `${b.name.toUpperCase()}`
if (nameB > nameA) {
return 1;
}
if (nameB < nameA) {
return -1;
}
return 0;
}),
nameIsOrdered: true,
codeIsOrdered: false,
priorityIsOrdered: false
})
}
}
orderByCode(){
let alarms = this.state.alarms
if(this.state.codeIsOrdered === true){
this.setState({
alarms: alarms.sort((a, b) => b.code - a.code),
codeIsOrdered: false,
nameIsOrdered: false,
priorityIsOrdered: false
})
}
else{
this.setState({
alarms: alarms.sort((a, b) => a.code - b.code),
codeIsOrdered: true,
nameIsOrdered: false,
priorityIsOrdered: false
})
}
}
orderByPriority(){
let alarms = this.state.alarms
if(this.state.priorityIsOrdered === true){
this.setState({
alarms: alarms.sort((a, b) => b.priority - a.priority),
nameIsOrdered: false,
codeIsOrdered: false
})
}
else{
this.setState({
alarms: alarms.sort((a, b) => a.priority - b.priority),
nameIsOrdered: false,
codeIsOrdered: false
})
}
this.setState(prevState => ({
priorityIsOrdered: !prevState.priorityIsOrdered,
}))
}
render(){
const alarms = this.state.alarms,
alarmsPaginate = this.state.alarmsPaginate;
return(
this.props.user.groups == 1 ? (
<div className="contactto-middle-content">
<ManagementMenu/>
<div className="management-content">
<div className="list-management-subtitle">ALARMES</div>
<button type="button" className="btn btn--save-attend-or-add-someone btn--color-tecno" onClick={() => browserHistory.push('/app/alarms/form/add')}>
<div><span className="btn--bold">+ ADICIONAR</span> ALARME</div>
</button>
{alarms &&
<div className="list-table">
<Paginate outerClass="paginate-wrapper" filterElements={this.getAlarmsPaginate} maxElements={5} elements={alarms} />
<div className="list-table-header">
<div className="left list-table--200"><span className="icons-order clickable" onClick={this.orderByName}>Nome<Order width="15" height="10"/></span></div>
<div className="left list-table--200"><span className="icons-order clickable" onClick={this.orderByCode}>Código<Order width="15" height="10"/></span></div>
<div className="left list-table--200"><span className="icons-order clickable" onClick={this.orderByPriority}>Prioridade<Order width="15" height="10"/></span></div>
<div className="list-table-body-column--action--2icons"></div>
</div>
<div className="list-table-body scroll">
{alarmsPaginate && alarmsPaginate.map((alarm) =>
<AlarmRow key={alarm.code} alarm={alarm} getAlarms={this.getAlarms} channelId={this.props.channelId} iconDetail={this.refs["iconDetail"]}/>
)}
</div>
</div>
}
</div>
</div>
):
<div className="error">Página não pode ser acessada, pois você não é um administrador</div>
)
}
}
export default AlarmManagement
And here is my Paginate component:
export default class Paginate extends React.Component{
constructor(props){
super(props)
this.state = {
pagesNumber: undefined,
elements: undefined,
elementsNumber: undefined,
startNumber: undefined,
endNumber: undefined,
pos: undefined,
}
this.nextList = this.nextList.bind(this)
this.previousList = this.previousList.bind(this)
}
previousList(){
if(this.state.pos > 1){
let pos = this.state.pos - 1
this.changePage(pos)
}
}
nextList(){
if(this.state.pos < this.state.pagesNumber){
let pos = this.state.pos + 1
this.changePage(pos)
}
}
changePage(pos){
const newStartNumber = pos === 1 ? 0 : this.props.maxElements * (pos - 1);
const newEndNumber = this.props.maxElements * pos > this.state.elementsNumber ? this.state.elementsNumber : this.props.maxElements * pos;
this.props.filterElements(newStartNumber, newEndNumber);
this.setState({
pos: pos,
startNumber: newStartNumber,
endNumber: newEndNumber,
});
}
componentWillReceiveProps(nextProps){
console.log(nextProps.elements != this.props.elements ? 'different' : 'equal')
}
componentWillMount(){
this.setState((prevState, props) => ({
pagesNumber: props.elements ? Math.ceil(props.elements.length / props.maxElements) : 0,
elements: props.elements,
elementsNumber: props.elements.length,
startNumber: 1,
endNumber: props.maxElements,
pos: 1,
}));
if(this.props.filterElements){
this.props.filterElements(0, this.props.maxElements)
}
}
render(){
const elementsNumber = this.state.elementsNumber,
startNumber = this.state.startNumber,
endNumber = this.state.endNumber;
return(
<div className={this.props.outerClass}>
{elementsNumber > this.props.maxElements &&
<div className="left contactto-100">
{elementsNumber && <span className="left">{`${startNumber === 0 ? 1 : startNumber}-${endNumber} de ${elementsNumber}`}</span>}
<span className="paginate-arrow paginate-arrow-left" onClick={this.previousList}></span>
<span className="paginate-arrow paginate-arrow-right" onClick={this.nextList}></span>
</div>
}
</div>
)
}
}
Paginate.defaultProps = {
outerClass: 'paginate-wrapper'
}
Paginate.propTypes = {
outerClass: React.PropTypes.string,
filterElements: React.PropTypes.func.isRequired,
maxElements: React.PropTypes.number.isRequired,
elements: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
}
The elements logged at componentWillReceive props are the same.
Code updated following the post advices, but:
componentWillReceiveProps(nextProps){
console.log(nextProps.elements != this.props.elements ? 'different' : 'equal') // still are equal
}
What is wrong?
Thanks in advance.
It is not very clear what your question is, or how your code is intended to function. However, you are doing some very inadvisable things within your code...
changePage(pos){
this.state.startNumber = pos === 1 ? 0 : this.props.maxElements * (pos - 1);
this.state.endNumber = this.props.maxElements * pos > this.state.elementsNumber ? this.state.elementsNumber : this.props.maxElements * pos;
this.props.filterElements(this.state.elements, this.state.startNumber, this.state.endNumber);
this.setState({
pos: pos,
startNumber: this.state.startNumber,
endNumber: this.state.endNumber
});
}
Although you are correctly using setState() to update state at the end of the function, you are directly mutating state before this. You can remedy this by either using a temporary variable to hold the new state, or by doing the calculations within the setState call. E.g.
changePage(pos){
const newStartNumber = pos === 1 ? 0 : this.props.maxElements * (pos - 1);
const newEndNumber = this.props.maxElements * pos > this.state.elementsNumber ? this.state.elementsNumber : this.props.maxElements * pos;
this.props.filterElements(this.state.elements, newStartNumber, newEndNumber);
this.setState({
pos: pos,
startNumber: newStartNumber,
endNumber: newEndNumber
});
}
The use of props within the constructor or componentWillReceiveProps() methods to set state is also something of an anti-pattern. In general in a React application we want to have a single source of truth - i.e. all data is the responsibility of one single component, and only one component. It is the responsibility of this component to store the data within its state, and distribute the data to other components via props.
When you do this...
constructor(props){
super(props)
this.state = {
elements: this.props.elements,
...
}
...
}
The parent and child component are now both managing elements within their state. If we update state.elements in the child, these changes are not reflected in the parent. We have lost our single source of truth, and it becomes increasingly difficult to track the flow of data through our application.
In your specific case, it is the responsibility of the Parent component to maintain elements within its state. The Child component receives elements as props - it should not store them as state or directly update elements in any way. If an action on Child should require an update of elements, this needs to be done via a function passed to it from Parent as props.
The changePage function I used as an example above could then become...
changePage(pos){
const newStartNumber = pos === 1 ? 0 : this.props.maxElements * (pos - 1);
const newEndNumber = this.props.maxElements * pos > this.props.elements.length ? this.props.elements.length : this.props.maxElements * pos;
this.props.filterElements(this.props.elements, newStartNumber, newEndNumber);
this.setState({
pos: pos,
startNumber: newStartNumber,
endNumber: newEndNumber
});
}
However, we can go even further - why do we need to pass this.props.elements to the filterElements function? The Parent component must already have a reference to elements; after all it gave it to us in the first place!
changePage(pos){
const newStartNumber = pos === 1 ? 0 : this.props.maxElements * (pos - 1);
const newEndNumber = this.props.maxElements * pos > this.props.elements.length ? this.props.elements.length : this.props.maxElements * pos;
this.props.filterElements(newStartNumber, newEndNumber);
this.setState({
pos: pos,
startNumber: newStartNumber,
endNumber: newEndNumber
});
}
In your Parent component you would then change the function...
getAlarmsPaginate(alarms, page, amount) {
this.state.alarmsPaginate = alarms.slice(page, amount)
this.setState({ alarmsPaginate: this.state.alarmsPaginate })
}
into...
getAlarmsPaginate(page, amount) {
const newAlarmsPaginate = this.state.alarms.slice(page, amount);
this.setState({ alarmsPaginate: newAlarmsPaginate });
}
Note we are slicing this.state.alarms directly, rather than a function argument - also we are no longer mutating our state - but using the setState function exclusively.
There are several more instances where you use props and state inappropriately throughout your code - I would go through it all and ensure that you follow the guidelines I have set out above - or even better go and read the React Documentation. You may well find that your issue resolves when you follow these practices, but if not then post back here with your revised code and I will be glad to help further.
Edit/Sample Code
Parent Component:
class Parent extends React.Component {
constructor() {
super();
// our parent maintains an array of all elements in its state,
// as well as the current page, and the number of items per page.
// getSampleData can be seen in the fiddle - it just makes an array
// of objects for us to render
this.state = {
allElements: getSampleData(),
page: 1,
numPerPage: 10
};
}
// we pass this function as a prop to our Paginate component to allow
// it to update the state of Parent
setPage(pageNum) {
this.setState({
page: pageNum
});
}
render() {
// get the appropriate elements from our own state
const firstItem = (this.state.page - 1) * this.state.numPerPage;
const lastItem = this.state.page * this.state.numPerPage;
const elementRender =
this.state.allElements
.slice(firstItem, lastItem)
.map(element => {
return (
<div key={element.itemNumber}>{element.itemName}</div>
);
});
// numberOfElements, numPerPage and the setPage function from
// Parent's state are passed
// to the paginate component as props
return (
<div>
<Paginate
numberOfElements={this.state.allElements.length}
numPerPage={this.state.numPerPage}
setPage={this.setPage.bind(this)}
/>
{elementRender}
</div>
);
}
}
Paginate Component:
class Paginate extends React.Component {
render() {
const numberOfButtons = Math.ceil(this.props.numberOfElements / this.props.numPerPage);
const buttons = [];
// make a first page button
buttons.push(<button key={0} onClick={() => this.props.setPage(1)}>First Page</button>);
let i = 0;
// add a button for each page we need
while (i < numberOfButtons) {
const page = ++i;
buttons.push(
<button
key={i}
onClick={() => this.props.setPage(page)}
>
Page {i}
</button>
);
}
// add a last page button
buttons.push(<button key={i+1} onClick={() => this.props.setPage(i)}>Last Page</button>);
return (
<div>
{buttons}
</div>
);
}
}
JSFiddle

Resources