React on render shows previous state value - reactjs

I'm a beginner for ReactJS, just started learning and started writing code for guessing numbers but the guess count shows different values. {this.state.attempts} holds the no. of attempts it took the user to find the answer to show the correct value. But {this.state.result} shows the result on each click but if user finds the answer it shows the previous state. I'm wondering how this happens. Is that because it's not under render()?
import React, { Component } from 'react';
export default class NoLifeCycComps extends Component {
constructor(props) {
super(props);
this.state = this.getInitialState();
this.checkValue = this.checkValue.bind(this);
this.updateInput = this.updateInput.bind(this);
}
randNum(){
return Math.floor((Math.random() * 100) + 1);
}
getInitialState(){
return {
num: this.randNum(),
inputValue: '',
attempts: 0,
result: '',
reset : false
}
}
reset() {
this.setState(this.getInitialState());
}
checkValue() {
this.setState((prevState) => {
return { attempts: prevState.attempts + 1 }
});
if (this.state.inputValue > this.state.num) {
this.state.result = "higher";
} else if (this.state.inputValue < this.state.num) {
this.state.result = "lesser";
} else if (this.state.inputValue == this.state.num) {
this.state.result = "you found it on " + this.state.attempts + "attempt(s)";
this.state.reset = true;
}
}
updateInput(e) {
this.setState({ inputValue: e.target.value })
}
render() {
return (
<div className = "numberGuess">
<h3> Guess the number </h3>
<input type = "text" value = { this.state.inputValue } onChange = { this.updateInput }/>
{this.state.reset ? <button onClick = { () => this.reset() }> start again! </button> : <button onClick = { () => this.checkValue() }> click </button>}
No.of Attempts took: { this.state.attempts } <br/>
<span> { this.state.result } </span>
</div>
);
}
}

setState is a async function. Next statement of setState may not have updated state value. Also I found mutation state update in your code. Please avoid mutation update. You should update all state using setState
Example
checkValue() {
let {
attempts,
inputValue,
num,
result,
reset
} = this.state;
attempts++;
if (inputValue > num) {
result = "higher";
} else if (inputValue < num) {
result = "lesser";
} else if (inputValue == num) {
result = "you found it on " + attempts + "attempt(s)";
reset = true;
}
this.setState({
attempts,
result,
reset
});
}
In this example we storing existing state value in variables and doing operation on that. At the end of calculation we are updating state once.

Related

Disabling button based on child component state in React

I am using a countdown component as a child component.
I want to disable/reable a button based on the state value of the counter, but I can't seem to read the value correctly.
This is what I have tried.
This is the countdown component:
import React from "react";
import PropTypes from "prop-types";
export default class Counter extends React.Component {
constructor() {
super();
this.state = { time: {}, seconds: 15 };
this.timer = 0;
this.startTimer = this.startTimer.bind(this);
this.countDown = this.countDown.bind(this);
}
secondsToTime(secs){
let hours = Math.floor(secs / (60 * 60));
let divisor_for_minutes = secs % (60 * 60);
let minutes = Math.floor(divisor_for_minutes / 60);
let divisor_for_seconds = divisor_for_minutes % 60;
let seconds = Math.ceil(divisor_for_seconds);
let obj = {
"h": hours,
"m": minutes,
"s": seconds
};
return obj;
}
componentDidMount() {
let timeLeftVar = this.secondsToTime(this.state.seconds);
this.setState({ time: timeLeftVar });
}
startTimer() {
if (this.timer === 0 && this.state.seconds > 0) {
this.timer = setInterval(this.countDown, 1000);
} else if ((this.timer === 0 && this.state.seconds === 0)){
this.state.seconds = 15;
this.timer = setInterval(this.countDown, 1000);
}
}
countDown() {
// Remove one second, set state so a re-render happens.
let seconds = this.state.seconds - 1;
this.setState({
time: this.secondsToTime(seconds),
seconds: seconds,
});
// Check if we're at zero.
if (seconds === 0) {
clearInterval(this.timer);
this.timer = 0;
console.log("counter is 0");
console.log(this.state.seconds);
console.log(this.timer);
}
}
render() {
this.startTimer();
return(
<span className={
this.state.seconds === 0 ? 'timerHidden' : 'timerActive'
}>
({this.state.time.s})
</span>
);
}
}
And how I read it and reset it in the parent component:
import Counter from '../Counter/Counter.js';
export default class Verify extends React.Component {
state = {
username: this.username,
email: this.email,
code: ""
};
constructor(props) {
super(props);
this.child = React.createRef();
}
resetTimer = () => {
this.child.current.startTimer();
};
resendConfirmationCode = async e =>{
this.resetTimer();
...
}
return (
<button
className="btn btn-primary register empty"
type="button"
disabled={this.child.current.seconds > 0}
onClick={this.resendConfirmationCode}>Resend code <Counter ref={this.child}/>
</button>
);
Inserting the counter works fine, reseting also, but the disabling of the button throws the following error:
TypeError: Cannot read property 'seconds' of null
Verify.render
> 109 | disabled={this.child.current.seconds > 0}
The this.child ref will be null/undefined on the initial render. Since you probably also want to disable the button if the counter component isn't available for some reason, you can just check if the ref's current value is falsey or if it is truthy and state.seconds of the child greater than 0.
<button
...
disabled={!this.child.current || this.child.current.state.seconds > 0}
onClick={this.resendConfirmationCode}
>
Resend code
</button>
<Counter ref={this.child} />
If we invert the second condition we can combine them into a single comparison using Optional Chaining.
<button
...
disabled={!this.child.current?.state.seconds <= 0}
onClick={this.resendConfirmationCode}
>
Resend code
</button>
<Counter ref={this.child} />

call function from another function in react shows "this.updateTitle is not defined" even when I bind the function in my constructor

I am trying to call a function inside of another function. I have binded the only function I am calling inside of the constructor(this.updateTitle) and it still wont get called("function not defined"). I am showing the entire script in case I am missing something crucial from it for why this is happening as this is my first project in react I am making with decent JavaScript experience. If someone could please explain why binding is not working and why my function is not being called, it would be appreciated. Also, an important note is that the functions called from componenetDidMount() WORKS but the other functions I call NOT in componenetDidMount() do NOT WORK. Please control F this.update.title and you will see where they are being called.
//fixed it. this is easier.
import React from 'react';
import '../css/style.css';
import Header from '../header/header';
import Footer from '../footer/footer';
export default class Post extends React.Component {
constructor(props) {
super(props);
this.state = {
arr: ["https://cdn.pixabay.com/photo/2018/09/05/19/25/pink-rose-on-empty-swing-3656894_" +
"1280.jpg"],
moveCount: 0,
title: null,
body: null
}
}
// choose an image from
// desktop**********************************************************************
// * ******************************8
clickImage = () => {
document
.getElementById("submitImage")
.click();
}
// updating
// state************************************************************************
// * *****************************************8
//updtae src title -- update
updateTitle = (value) => {
this.setState({title: value});
}
//update body -- update
updateBody = (value) => {
this.setState({body: value});
}
//add source to array -- update
addSrcArray = (src) => {
this.setState({
arr: [
...this.state.arr,
src
]
});
}
//remove soure form array -- update
removeSrcArray = (stateArr, src) => {
var temp = stateArr;
temp.splice(temp.indexOf(src), 1);
this.setState({arr: temp});
}
// functions from componenet did mount that updates
// above************************************************************************
// * ********************8
//add the title
pushTitle = () => {
var value = document.getElementsByClassName("enterPostTitle")[0].value;
if (value.length === 0) {
document.getElementsByClassName("postTitle")[0].innerText = "Title...";
this.updateTitle(null); //update
return;
}
document.getElementsByClassName("postTitle")[0].innerText = value;
this.updateTitle(value); //update
}
//add the body
pushBody = () => {
var value = document.getElementsByClassName("enterPostBody")[0].value;
if (value.length === 0) {
document.getElementsByClassName("postBodyContent")[0].innerText = "Body...";
this.updateBody(null); //update
return;
}
document.getElementsByClassName("postBodyContent")[0].innerText = value;
this.updateBody(value); //update
}
//add the source
pushSource = (fileUpload) => {
if (fileUpload.files && fileUpload.files[0] && (fileUpload.files[0].type.toLowerCase() === "image/jpeg" || fileUpload.files[0].type.toLowerCase() === "image/png")) {
var reader = new FileReader();
reader.onload = (e) => {
if (this.state.arr.includes(e.target.result)) {
return;
}
this.addSrcArray(e.target.result); //update
document.getElementsByClassName("postPicture")[0].src = this.state.arr[this.state.arr.length - 1];
this.state.moveCount = this.state.arr.length - 1;
};
reader.readAsDataURL(fileUpload.files[0]);
}
}
//remove the source
removeSource = (src) => {
if (this.state.arr.includes(src) && src !== "https://cdn.pixabay.com/photo/2018/09/05/19/25/pink-rose-on-empty-swing-3656894_" +
"1280.jpg") {
this.removeSrcArray(this.state.arr, src); //update
this.state.moveCount -= 1;
document.getElementsByClassName("postPicture")[0].src = this.state.arr[this.state.moveCount];
}
}
//move next and back in source array
moveBy = (move) => {
if (this.state.arr.length < 2) {
return;
}
if (move === "next") {
this.state.moveCount += 1;
if (this.state.moveCount === this.state.arr.length) {
this.state.moveCount = 0;
}
document.getElementsByClassName("postPicture")[0].src = this.state.arr[this.state.moveCount];
} else if (move === "back") {
this.state.moveCount -= 1;
if (this.state.moveCount === -1) {
this.state.moveCount = this.state.arr.length - 1
}
document.getElementsByClassName("postPicture")[0].src = this.state.arr[this.state.moveCount];
}
}
//check data and push to database
postDbnnmn = () => {
var errorArr = [];
if (this.state.arr === undefined || this.state.arr === null || this.state.arr.length < 1 || this.state.arr.length > 11) {
errorArr.push("pictureError-only up to 11 pictures allowed");
}
if (this.state.body === undefined || this.state.body === null || this.state.body.length > 3000 || this.state.body.length < 15) {
errorArr.push("bodyError-body must be 15 to 3000 characters");
}
if (this.state.title === undefined || this.state.title === null || this.state.title.length > 30 || this.state.title.length < 3) {
errorArr.push("titleError-title must be 3 and 30 characters");
}
if(errorArr.length > 0) {
errorArr.map(error => {
document.getElementById(error.split("-")[0]).innerText = errorArr.split("-")[1]; // could do in above but just keeping clean
})
} else {
this.submitForm();
}
}
submitForm = () => {
//get information and use ajax and submit -- run errors on back
}
// on mount listen for
// events***********************************************************************
// * ******************************************8
componentDidMount = () => {
document
.getElementById("root")
.classList
.add("changeBackground");
//submit form here using ajax
}
render() {
return (
<div>
<Header/>
<div className="postMain">
<div className="postInformation">
<h1>Post</h1>
<input
className="enterPostTitle postFromInfoInput"
placeholder="Enter post title"
onKeyUp={this.pushTitle}></input>
<div>
<h1 className="postTitle" hidden>Title...</h1>
</div>
<textarea
rows="12"
className="enterPostBody postFromInfoInputB"
placeholder="Enter post body"
onKeyUp={this.pushBody}></textarea>
</div>
<div className="postBody" hidden>
<h2 className="postBodyContent">Body...</h2>
</div>
<div className="postPictureContent">
<input
className="enterPostImages postFromInfoInputImage"
type="file"
id="submitImage"
onChange=
{(e) => {this.pushSource(e.target)}}
hidden></input>
<button className="postPictureButton" onClick={this.clickImage}>Add Art</button>
<div className="postButtonWrap">
<button
className="postPictureButton back"
onClick=
{(e) => {this.moveBy("back")}}>back</button>
<button
className="postPictureButton next"
onClick=
{(e) => {this.moveBy("next")}}>next</button>
</div>
<img
className="postPicture"
src="https://cdn.pixabay.com/photo/2018/09/05/19/25/pink-rose-on-empty-swing-3656894_1280.jpg"
onClick=
{(e) => { this.removeSource(e.target.src) }}></img>
</div>
<div className="postFormButton">
<button className="postButton postFromInfoButton" onClick={this.postDbnnmn}>
Post
</button>
</div>
</div>
<Footer/>
</div>
);
}
}

React: Cannot show button when element is true

this is my code:
const Agregar = ({inputPregunta, validatePregunta}) => {
return(
<div id="content">
<h2>¿Cual es tu pregunta?</h2>
<input id="valorPregunta" type="text" placeholder="Añade aqui tu pregunta..." onChange={(e) => inputPregunta(e)}/>
{validatePregunta && <button>Agregar</button>}
</div>
);
}
What i am trying to do is when the input has something entered the prop validatePregunta (default is false) comes to true and the button element shows, for that i tried to do a method in the App.js file like this:
actualizarPregunta = (e) => {
this.setState({inputPregunta: e.target.value})
if(this.state.inputPregunta.trim().length > 0){
this.setState({validatePregunta: true})
} else {
this.setState({validatePregunta: false})
}
}
But nothing shows, is there's something that i am doing wrong?
Edit: Here is the code of the rendering for the props:
renderRoute = () => {
switch(this.state.route) {
case 'menu':
return (
<div>
<Header />
<Agregar inputPregunta={this.actualizarPregunta} validate={this.state.validatePregunta}/>
<Publications />
</div>
)
default :
return (
<div>
<Header />
<Publications />
</div>
)
}
}
render() {
return (
<div>
{
this.renderRoute(this.state.route)
}
</div>
);
}
This is how you use the compoennt
<Agregar inputPregunta={this.actualizarPregunta} validate={this.state.validatePregunta}/>
you are passing the value of this.state.validatePregunta by property name of validate
and then you are expecting something validatePregunta in component Agregar, but it should be actually validate
const Agregar = ({inputPregunta, validatePregunta}) => { //incorrect
const Agregar = ({inputPregunta, validate}) => { //correct
OR else simply change the prop name as below
<Agregar inputPregunta={this.actualizarPregunta} validatePregunta={this.state.validatePregunta}/>
And you should change actualizarPregunta once you update the state it's not updating the state value real time, its a async process, so this.state.inputPregunta.trim() will give you the value just before the update, so change it like this, and i love the way #Drew Reese handle this part
actualizarPregunta = (e) => {
const newValue = e.target.value;
this.setState({inputPregunta: newValue })
if(newValue.trim().length > 0){
this.setState({validatePregunta: true})
} else {
this.setState({validatePregunta: false})
}
}
Issue 1
The props don't align.
The component signature is const Agregar = ({inputPregunta, validatePregunta}) but you pass validate={this.state.validatePregunta}.
<Agregar
inputPregunta={this.actualizarPregunta}
validate={this.state.validatePregunta}
/>
Solution
Align on prop name usage.
<Agregar
inputPregunta={this.actualizarPregunta}
validatePregunta={this.state.validatePregunta}
/>
Issue 2
React component state lifecycle. The enqueued state is attempted to be referenced within the same react cycle that enqueue it. You need the enqueued value which won't be available until the next render cycle.
actualizarPregunta = (e) => {
this.setState({ inputPregunta: e.target.value }); // <-- state update enqueued
if (this.state.inputPregunta.trim().length > 0){ // <-- current state value
this.setState({ validatePregunta: true })
} else {
this.setState({ validatePregunta: false })
}
}
Solution 1
Enqueue the state update and use the current event value to set the other state
actualizarPregunta = (e) => {
const { value } = e.target;
this.setState({
inputPregunta: value,
validatePregunta: !!value.trim().length, // <-- *
});
}
* length == 0 is falsey, length !== 0 is truthy, and coerced to boolean (!!)
Solution 2
Enqueue the state update and use componentDidUpdate lifecycle function to handle effect
actualizarPregunta = (e) => {
const { value } = e.target;
this.setState({ inputPregunta: value });
}
componentDidUpdate(prevProps, prevState) {
const { inputPregunta } = this.state;
if (prevState.inputPregunta !== inputPregunta) {
this.setState({ validatePregunta: !!inputPregunta.trim().length });
}
}
Solution #1 is the simpler and more straight forward.

Clear Interval not working in my component JavaScript?

I have a component that will start counting down from 5 mins when a timestamp (obsTakenTime) is received via props. When the countdown gets <=0, I render ‘Overdue’. At this point I need to clear interval which I think I've done, the issues is when if, I refresh the page the obstimeleft should remain overdue but the countdown automatically starts from 59 mins because the value of nextObservationTime becomes 59min and this.state.obsTimeleft becomes undefined even thought the value of timestamp obsTakenTime is the same. I've looked at other similar threads on SO but I couldn't get mine to work. Any help is appreciated.
similar post - Clear interval in React class
Countdown component
export default class ObservationCountDown extends React.Component {
constructor(props) {
super(props);
this.state = {
obsTimeleft: undefined
};
this.countDown = this.countDown.bind(this);
this.startCounter = this.startCounter.bind(this);
this.countDownInterval = null;
}
countDown() {
const { obsTakenTime} = this.props; //when last obs was taken by the user in ms
const nextDueTimeForObs = moment(obsTakenTime).add(5, 'minutes');
const nextObservationTime = Number(nextDueTimeForObs.subtract(moment.now()).format('m'));
const timeToDisable = 2; // disable buttons time
this.setState({ obsTimeleft: nextObservationTime + ' min' }, () => {
if (nextObservationTime <= Number(timeToDisable)) {
this.props.disablePatientUpdate();
}
if (nextObservationTime <= 0) {
clearInterval(this.countDownInterval); // doesn't work here
this.setState({ obsTimeleft: 'Overdue' }, () => {
if(this.state.obsTimeleft === 'Overdue'){
clearInterval(this.countDownInterval); // doesn't work here
}
});
}
});
}
componentDidMount() {
this.startCounter();
}
startCounter() {
this.countDownInterval = setInterval(this.countDown, 1000);
}
componentDidUpdate(prevProps){
if(this.props.patient.timestamp !== prevProps.patient.timestamp){
this.startCountdown();
}
}
componentWillUnmount(){
clearInterval(this.countDownInterval);
}
render() {
const { obsTimeleft } = this.state;
return (
<>
{(obsTimeleft && obsTimeleft === 'Overdue') ?
<div className="text-danger">
<strong>{obsTimeleft}</strong>
</div> :
<div>
<strong>{.obsTimeleft}</strong>
</div>}
</>
);
}
}
another version of countDown() that I tried and didn't work
countDown() {
const { obsTakenTime } = this.props; // obs duration - when last obs was taken by the user in min
const nextDueTimeForObs = moment(obsTakenTime).add(2, 'minutes');
const nextObservationTime = Number(nextDueTimeForObs.subtract(moment.now()).format('m'));
console.log('nextObservationTime', nextObservationTime);
this.setState({ obsTimeleft: nextObservationTime + ' min' })
if (nextObservationTime <= 0) {
this.setState({ obsTimeleft: 'Overdue' }, () => {
if(this.state.obsTimeleft === 'Overdue') {
clearInterval(this.countDownInterval);
}
});
this.props.enablePatientUpdate();
this.props.resetPatient(patient);
}
}

Reactjs show hide multiple components

Newbie React question here on show hide functionality.
I have a state of 'show' that I set to false:
this.state = {
show: false,
};
Then I use the following function to toggle
toggleDiv = () => {
const { show } = this.state;
this.setState({ show : !show })
}
And my display is
{this.state.show && <xxxxxx> }
This all works fine. However I want to apply the function it to multiple cases (similar to accordion, without the closing of other children. So I change my constructor to
this.state = {
show: [false,false,false,false,false,false]
};
and this to recognise there are 6 different 'shows'.
{this.state.show[0] && <xxxxxx> }
{this.state.show[1] && <xxxxxx> } etc
But where I get stuck is how to account for them in my toggleDiv function. How do I insert the square bracket reference to the index of show (if this is my problem)?
toggleDiv = () => {
const { show } = this.state;
this.setState({ show : !show })
}
Thanks for looking.
First of all I'd suggest you not to rely on current state in setState function, but to use the callback option to be 100% sure that you are addressing to the newest state:
this.setState((prevState) => ({ show: !prevState.show }));
How to deal with multiple elements?
You'll have to pass the index of currently clicked element.
{yourElements.map((elem, i) => <YourElem onClick={this.toggleDiv(i)} />)}
and then inside your toggleDiv function:
toggleDiv = (i) => () => {
this.setState((prevState) => {
const r = [...prevState.show]; // create a copy to avoid state mutation
r[i] = !prevState.show[i];
return {
show: r,
}
}
}
Use an array instead of a single value. In your toggle div function make a copy of the state array make necessary changes and push the entire array back up to state at the end.
This is some simplified code showing the workflow I described above
export default class myClass extends React.Component{
constructor(props){
super(props);
this.state = { show: new Array(2).fill(false) };
}
//you need a index or id to use this method
toggleDiv = (index) => {
var clone = Object.assign( {}, this.state.show ); //ES6 Clones Object
switch(clone[index]){
case false:
clone[index] = true
break;
case true:
clone[index] = false
break;
}
this.setState({ show: clone });
}
render(){
return(
<div>
{ this.state.show[0] && <div> First Div </div> }
{ this.state.show[1] && <div> Second Div </div> }
{ this.state.show[2] && <div> Third Div </div> }
</div>
)
}
}

Resources