React props are updating automatically - reactjs

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

Related

React : Prop not updated inside of a map function

Edit : Codesandbox here
Here is a simplified version on my parent component :
export default function Parent() {
// State
const [status, setStatus] = useState(1);
const [source, setSource] = useState('https://packagingeurope.com/downloads/7475/download/danone-05.03.20.jpg');
const [steps, setSteps] = useState([
{
title: 'Prediction Initiated',
value: 1,
loading: false,
completed: false,
active: false,
},
{
title: 'Prediction in Progress',
value: 2,
loading: false,
completed: false,
active: false,
},
{
title: 'Prediction Finished',
value: 3,
loading: false,
completed: false,
active: false,
},
]);
useEffect(() => {
if (status) {
const newSteps = steps;
newSteps[status - 1].active = true;
if (status > 1) {
newSteps[status - 2].completed = true;
}
if (status === 3) {
newSteps[status - 1].active = false;
newSteps[status - 1].completed = true;
}
setSteps(newSteps);
} else {
// Do nothing
console.log('No status match');
}
},
[status]);
return (
<div className="container-fluid">
<Timeline status={status} steps={steps} source={source} />
</div>
);
}
And here is my child component :
export default class Timeline extends React.Component {
renderSteps() {
const { steps } = this.props;
return steps.map((step, index) => {
console.log(step);
console.log(steps);
return (
<div key={`test${index}`}>
{step.title}
{step.active && <span>is active</span>}
{step.loading && <span>is loading</span>}
{step.completed && <span>is completed</span>}
</div>
);
});
}
render() {
const { status, steps, source } = this.props;
return (
<div className="timeline">
{this.renderSteps()}
</div>
</>
);
}
}
When i console.log steps props inside my child component, I see that they are correctly updated. (When status === 1, the first step active property is true)
But when i console.log step (inside my map function), none of the properties are updated. (When status === 1, the first step active property is false)
You can see on the capture below that something is not right (I already encountered this problem but unfortunately I can't remember how I solved it, and I am wondering if it's because of the useEffect hooks which I didn't use before this project.
Thank you for your help
Edit : Codesandbox here
Ok, I figured it out.
Since I got this code from another developer, I didn't understand everything that was implemented. I went to the React documentation to understand where the problem was coming from.
By passing a second parameter to the useEffect function, React can skip the effect and will not re-render the component for performance issues if there is not any change. As my status doesn't change (I change it manually for the moment), my component was not refreshing.
By adding [steps] width [status] inthe second parameter, everything works.
useEffect(() => {
if (status) {
const newSteps = steps;
newSteps[status - 1].active = true;
if (status > 1) {
newSteps[status - 2].completed = true;
}
if (status === 3) {
newSteps[status - 1].active = false;
newSteps[status - 1].completed = true;
}
setSteps(newSteps);
} else {
// Do nothing
console.log('No status match');
}
}, [status, steps]);

React - update nested state (Class Component)

I'm trying to update a nested state. See below. The problem is that upon clicking on a category checkbox, instead of updating the {categories: ....} object in state, it creates a new object in state:
class AppBC extends React.Component {
constructor(props) {
super(props)
this.state = {
products: [],
categories: []
}
this.handleSelectCategory = this.handleSelectCategory.bind(this);
}
componentDidMount() {
this.setState({
products: data_products,
categories: data_categories.map(category => ({
...category,
selected: true
}))
});
}
handleSelectCategory(id) {
this.setState(prevState => ({
...prevState.categories.map(
category => {
if(category.id === id){
return {
...category,
selected: !category.selected,
}
}else{
return category;
} // else
} // category
) // map
}) // prevState function
) // setState
} // handleSelectCategory
render() {
return(
<div className="bc">
<h1>Bare Class Component</h1>
<div className="main-area">
<Products categories={this.state.categories} products={this.state.products} />
<Categories
categories={this.state.categories}
handleSelectCategory={this.handleSelectCategory}
/>
</div>
</div>
);
};
Initial state before clicking (all categories are selected):
After clicking on an a checkbox to select a particular category, it saves a new object to state (correctly reflecting the category selection) instead of updating the already existin categories property:
Change your update to:
handleSelectCategory(id) {
this.setState(prevState => ({
...prevState,
categories: prevstate.categories.map(
category => {
if (category.id === id) {
return {
...category,
selected: !category.selected,
}
} else {
return category;
} // else
} // category
) // map
}) // prevState function
) // setState
}
I prefer this way, it's more easy for reading
handleSelectCategory(id) {
const index = this.state.categories.findIndex(c => c.id === id);
const categories = [...this.state.categories];
categories[index].selected = !categories[index].selected;
this.setState({ categories });
}
If your purpose is to only change selected property on handleSelectCategory function,
Then you could just do it like
run findIndex on array and obtain index for id match from array of objects.
update selected property for that index
Code:
handleSelectCategory(id) {
let targetIndex = this.state.categories.findIndex((i) => i.id === id);
let updatedCategories = [...this.state.categories];
if (targetIndex !== -1) {
// this means there is a match
updatedCategories[targetIndex].selected = !updatedCategories[targetIndex].selected;
this.setState({
categories: updatedCategories,
});
} else {
// avoid any operation here if there is no "id" matched
}
}

React setState when score is 0

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.

Material UI linear progress animation when using data

The docs at material ui for reactJS proposes this sample code for determinate progress bars.
export default class LinearProgressExampleDeterminate extends React.Component {
constructor(props) {
super(props);
this.state = {
completed: 0,
};
}
componentDidMount() {
this.timer = setTimeout(() => this.progress(5), 1000);
}
componentWillUnmount() {
clearTimeout(this.timer);
}
progress(completed) {
if (completed > 100) {
this.setState({completed: 100});
} else {
this.setState({completed});
const diff = Math.random() * 10;
this.timer = setTimeout(() => this.progress(completed + diff), 1000);
}
}
render() {
return (
<LinearProgress mode="determinate" value={this.state.completed} />
);
}
}
This creates a loading animation until bar is full. I am trying to modify it to use data from a json file so that it stops at the value that I specified for it in each json item. I am getting that part right. That was the easy part. But the animation fails because the animation is scripted using the value of completed in the constructor's state. And it is also located outside of my data.map so I can seem to find the way to make it read the value in the json file so it can se it for it's timeout function. :(
This is what I have (reduced)
JSON
exports.playerItems = [
{
id: 283,
completed: "100",
}
{
id: 284,
completed: "70",
}
{
id: 285,
completed: "20",
}
...
{
id: 295,
completed: "50",
}
]
Data injection
import PLAYERS from 'data/players.js';
const playersData = PLAYERS['playerItems'];
And my table is mapped
<Table>
{playersData.map((row, index) => (
<TableRow id={row.name} key={index}>
<TableRowColumn>
<LinearProgress
mode="determinate"
value={row.completed} />
</TableRowColumn>
</TableRow>
))}
</Table>
How can I modify the progress() function so that it animates the value given to the LinearProgress?
Thanks in advance
You can apply a state change to an array of player data and continually update the array in increments until all of the players have rendered.
First, start from zero:
constructor(props) {
super(props);
this.state = {
playersData: data.map(item => ({ ...item, completed: 0}))
};
};
Then initiate progress at mount:
componentDidMount() {
this.timer = setTimeout(() => this.progress(5), 100);
}
Update until each player has reached 100%:
progress(completion) {
let done = 0;
this.setState({
playersData: data.map((item, i) => {
const { completed: current } = this.state.playersData[i];
const { completed: max } = item;
if (current + completion >= max) {
done += 1;
}
return {
...item,
completed: Math.min(current + completion, max),
};
}),
});
if (done < data.length) {
this.timer = setTimeout(() => this.progress(5), 100);
}
}
Adjust the delay and increment as you see fit. The limitation is that you need all of the player data that will be rendered and it needs to be in state as an array that is updated in a single setState
Here is a working example on codesandbox.

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/

Resources