Disabling button based on child component state in React - reactjs

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} />

Related

setState not setting some states

Im creating a clock that has minutes and seconds. This is how my component looks like
class Time extends React.PureComponent
{
constructor(props)
{
super(props);
this.state =
{
hrs: 0,
mins: 0,
secs: 0
}
this.tick = this.tick.bind(this);
}
tick()
{
//This increments the minutes
if(this.state.secs >= 59)
{
this.setState({mins: this.state.mins+1});
this.setState({secs: 0});
}
//This increments the hours
if(this.state.minutes >= 59)
{
this.setState({hrs: this.state.hrs+1});
this.setState({mins: 0});
}
else
{
//This increments the seconds
this.setState({secs: this.state.secs+1});
}
}
componentDidMount()
{
this.tock = setInterval(()=>
{
this.tick();
}, 1000)
}
componentWillUnmount()
{
clearInterval(this.tock);
}
render()
{
return(
<div>
<h1>Time</h1>
<section>
{this.state.hrs}:{this.state.mins}:{this.state.secs}
</section>
</div>
);
}
}
}
export default Time;
The mins do increment to 59 and in turn increment the hrs - then reset to 0. But the secs just keep going and never reset to 0. They do update the mins though. The problem is that they go from 0 to infinity and dont stop counting. How can I make them reset to 0 each time?
setState is asynchronous, so you cannot ensure all the state updates in the sequence with multiple setState. I'd suggest that you should have only one setState at the end of that function after all secs, mins, and hrs computed.
Side note I also fixed your wrong value in this.state.minutes to this.state.mins.
class Time extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
hrs: 0,
mins: 0,
secs: 0,
};
this.tick = this.tick.bind(this);
}
tick() {
let { secs, mins, hrs } = this.state;
secs += 1;
if (secs > 59) {
mins += 1;
secs = 0;
}
if (mins > 59) {
hrs += 1;
mins = 0;
}
this.setState({ secs, mins, hrs });
}
componentDidMount() {
this.tock = setInterval(() => {
this.tick();
}, 1000);
}
componentWillUnmount() {
clearInterval(this.tock);
}
render() {
return (
<div>
<h1>Time</h1>
<section>
{this.state.hrs}:{this.state.mins}:{this.state.secs}
</section>
</div>
);
}
}
You could also use a functional based component to achieve the same effect.
function Time() {
const [time, setTime] = useState({
secs: 50,
mins: 58,
hrs: 0
});
const tick = () => {
setTime(prevState => ({...prevState, secs: prevState.secs + 1}));
}
useEffect(() => {
setInterval(() => {
tick()
}, 1000)
}, []);
useEffect(() => {
if(time.secs > 59) {
setTime(prevState => ({ ...prevState, secs: 0, mins: prevState.mins + 1 }));
}
}, [time.secs])
useEffect(() => {
if (time.mins > 59) {
setTime(prevState => ({ ...prevState, mins: 0, hrs: prevState.hrs + 1 }));
}
}, [time.mins])
return (
<div>
<h1>Time</h1>
<section>
{time.hrs}:{time.mins}:{time.secs}
</section>
</div>
);
}
export default Time;

ReactJS Jest Tesing - fireEvent.click() not triggering a button

I am doing testing first time in my life. I have a stopwatch app, and I want to check with test is secondmeter running. I am doing it by triggering the button and check is 00:00:00 still there.
Problem is those 00:00:00 are still in the span after triggering a button. Program working completely fine, not any problem at all. Where is the problem with this triggerin?
I have a button START for that:
class SecondTimer extends React.Component {
constructor(props) {
super(props);
this.state = {
running: false, // Format to false, so when start a program clock is not running
currentTimeMs: 0,
currentTimeSec: 0,
currentTimeMin: 0,
};
}
start = () => {
if (!this.state.running) {
this.setState({ running: true });
this.watch = setInterval(() => this.pace(), 10);
}
};
{this.state.running === false && (
<Button className={"start-stop-reset-nappi"} variant="success" size="lg" onClick=
{this.start}>START</Button>
)}
And I have span where time showing (value before starting is 00:00:00, picture of that below).
<span>
{this.props.formatTime(this.props.currentTimeMin)}:
{this.props.formatTime(this.props.currentTimeSec)}:
{this.props.formatTime(this.props.currentTimeMs)}
</span>
Then I have this test:
import { render, screen, fireEvent } from '#testing-library/react';
import SecondTimer from '../components/SecondTimer.jsx';
test('Test - Secondmeter running or not', () => {
render(<SecondTimer />);
const start = screen.getByText("START") // Here I'll check is there button (text) like START
fireEvent.click(start); // Then I "Click" it
const numbers = screen.getByText(/00:00:00/i) // Check is the 00:00:00 still on screen after triggering the button
expect(numbers).toBeInTheDocument()
});
Complete component code:
import React from 'react';
import SecondTimerNumbers from './SecondTimerNumbers.jsx';
import SecondTimerList from './SecondTimerList.jsx';
import { Button } from 'react-bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
class SecondTimer extends React.Component {
constructor(props) {
super(props);
// Start funktio määrittää running arvon -> true, joka kertoo sen, että kello käy. Tallentaa tänne ajan, josta SeconTimerNumber hakee tiedon
this.state = {
running: false, // Alustettu falseksi, eli ohjelman avatessa kello ei käy
currentTimeMs: 0,
currentTimeSec: 0,
currentTimeMin: 0,
};
}
// Muuttaa kelloajan numeraaliset luvut stringiksi, sekä lisää yhdet nollat lukuun, jotta luvut ei ns. pompi silmillä.
formatTime = (val, ...rest) => {
let value = val.toString();
if (value.length < 2) {
value = '0' + value;
}
if (rest[0] === 'ms' && value.length < 3) {
value = '0' + value;
}
return value;
};
// Start napista painalla käynnistää tämän funktion. Muuttaa state running trueksi, eli kello käy. SetInterval on Reactin oma sekuntikello, ja this.pace 10 määrittelee sadasosasekunnit
start = () => {
if (!this.state.running) {
this.setState({ running: true });
this.watch = setInterval(() => this.pace(), 10);
}
};
// Stop painikkeesta muuttaa running staten falseksi -> kello pysähtyy, sekä tyhjentää sekuntikellon (pysäyttää).
stop = () => {
this.setState({ running: false });
clearInterval(this.watch);
};
// Määrittelee kellon toiminnan
pace = () => {
this.setState({ currentTimeMs: this.state.currentTimeMs + 10 }); // Lisää 10ms kerrallaan
// Kun millisekunteja 1000, lisää yhden sekunnin, sekä muuttaa millisekuntien arvoksi 0 kierroksen jälkeen
if (this.state.currentTimeMs >= 1000) {
this.setState({ currentTimeSec: this.state.currentTimeSec + 1 });
this.setState({ currentTimeMs: 0 });
}
// Kun sekunteja on 60, lisää yhden minuutin, sekä muuttaa sekuntien arvoksi 0 kierroksen jälkeen
if (this.state.currentTimeSec >= 60) {
this.setState({ currentTimeMin: this.state.currentTimeMin + 1 });
this.setState({ currentTimeSec: 0 });
}
};
// Määrittää (resetoi) staten allaolevat arvot nollaksi, jolloin sekuntikello voidaan taas aloittaa alusta
resetTime = () => {
this.setState({
currentTimeMs: 0,
currentTimeSec: 0,
currentTimeMin: 0,
});
};
// UserInterface eli käyttöliittymä renderöityy tässä
render() {
return (
<div className={'main-div'}>
<div className={'left'}>
{this.state.running === false && (
<Button className={"start-stop-reset-nappi"} variant="success" size="lg" onClick={this.start}>START</Button>
)}
{this.state.running === true && (
<Button className={'start-stop-reset-nappi'} variant="warning" size="lg" onClick={this.stop}>PAUSE</Button>
)}
<Button className={'start-stop-reset-nappi'} variant="danger" size="lg" onClick={this.resetTime}>RESET</Button>
<div className={'tulostaulu-main'}>
<SecondTimerNumbers
ref="display"
{...this.state}
formatTime={this.formatTime}
/>
</div>
</div>
<div className={'right'}>
<SecondTimerList clickResetTime={this.resetTime} {...this.state} formatTime={this.formatTime} />
</div>
</div>
);// Yllä oleva ClickResetTime funktio on tämän sivut resetTime funktio.
// Tällä saan kutsuttua funktiota toisesta componentista, tavallaan "injektoin" funktion tähän componenttiin.
}
}
export default SecondTimer;
I don't really know what is your problem with your test. I've created a small test based on your current one which works really well.
The idea of the test is pretty simple. As I clicked the button, I expect to see the change in seconds. The key thing is to use waitFor api with a specified timeout option to make sure testing library resolving your UI change.
Here is the snippet:
import React from "react";
import { render, screen, fireEvent, waitFor } from "#testing-library/react";
import "#testing-library/jest-dom";
import { SecondTimer } from "./App";
test("should work", async () => {
render(<SecondTimer />);
const start = screen.getByText("START");
fireEvent.click(start);
await waitFor(() => screen.getByText(/00:02:00/i), {
timeout: 2000 // wait 2s
});
const numbers = screen.getByText(/00:02:00/i);
expect(numbers).toBeInTheDocument();
});
Here is the codesanbox which I've created exclusively for you (The setup to make the test able to run is based on create-react-app script):
https://codesandbox.io/s/morning-browser-92uks?file=/src/App.test.js
NOTE: To run the test, you click a Tests tab next to Browser on the preview area.

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);
}
}

React: setting setInterval conditionally

My timer never stops after reaching zero (it keeps counting down on screen). I did a console.log on timer and I see that, while the time on screen is counting down, console.log returns that timer never actually changes its value. Any ideas why? Am I not setting state properly or something like that?
class Clock extends Component {
constructor(props) {
super(props);
this.state = {break:5,
session:25,
timer: 1500}
this.handleClick = this.handleClick.bind(this);
this.handleTimer=this.handleTimer.bind(this);
}
handleClick(event){
const id= event.target.id;
let breakvar= this.state.break;
let sessionvar= this.state.session;
if (id==="break-increment" && breakvar<=59){
this.setState((state) => ({
break: this.state.break +1}));}
else if (id==="break-decrement" && breakvar>1){
this.setState((state) => ({
break: this.state.break -1}));}
else if(id==="reset"){
this.setState((state) => ({
break: 5, session: 25, timer: 1500}));
}
else if (id==="session-increment" && sessionvar <=59){
this.setState((state) => ({
session: this.state.session +1, timer: this.state.timer + 60}));}
else if(id==="session-decrement" && sessionvar>1){
this.setState((state) => ({
session: this.state.session -1, timer:this.state.timer - 60}));}
}
handleTimer(evt){
let timer=this.state.timer;
let Interval=setInterval(() => {
this.setState({
timer: this.state.timer - 1
})
console.log(timer) },1000)
if(timer ===0){
clearInterval(Interval)
}
}
Clock(){
let minutes = Math.floor(this.state.timer / 60);
let seconds = this.state.timer - minutes * 60;
seconds = seconds < 10 ? '0' + seconds : seconds;
minutes = minutes < 10 ? '0' + minutes : minutes;
return minutes + ':' + seconds;
}
render() {
return(
<div id="container">
<Display break={this.state.break} displayTime={this.Clock()} session={this.state.session}/>
<p id="break-label">Break length</p>
<Button onClick={this.handleClick} id="break-increment"/>
<Button onClick={this.handleClick} id="break-decrement"/>
<p id="session-label">Session length</p>
<Button onClick={this.handleClick} id="session-increment" />
<Button onClick={this.handleClick} id="session-decrement"/>
<Button onClick={this.handleTimer} id="start_stop"/>
<Button onClick={this.handleClick} id="reset"/>
</div>
)
}
This is a 'structural problem'.
You're checking condition in handler only once, not inside 'child function' invoked every second.
You need to move 'actions'/condition into 'interval body fn', sth like:
handleTimer(evt){
clearInterval(this.Interval)
this.Interval=setInterval(() => {
let timer=this.state.timer;
if(timer > 0){
this.setState({
timer: this.state.timer - 1
})
}else{
clearInterval(this.Interval)}
},1000)}
}

React on render shows previous state value

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.

Resources