React why my clearInterval cannot stop setInterval? - reactjs

I am totally new to React. What I try to implement is a timer. When click on the hours, minute or second the timer will stop and for the selected one turns into an input field when enter button has been click it should not show any more input fields and start back the clock.
how it looks like
I try to stop passing new props to the child component when I click the flexbox container. I wrote a handleClick function and setInterval() or clearInterval() base on update variable in the state.
What I want is when I click any of the hour/minute/second, the select filed will change to the input filed, and the timer stop. Once I hit enter it will back to the timer.
class Timer extends React.Component{
constructor(props){
super (props);
this.timerRef = React.createRef();
this.state = {
hh: new Date().getHours().toString().padStart(2,"0"),
mm: new Date().getMinutes().toString().padStart(2,"0"),
ss: new Date().getSeconds().toString().padStart(2,"0"),
suffix: new Date().getHours() > 12 ? "PM" : "AM",
update:true,
};
}
tick() {
this.setState({
hh: new Date().getHours().toString().padStart(2,"0"),
mm: new Date().getMinutes().toString().padStart(2,"0"),
ss: new Date().getSeconds().toString().padStart(2,"0"),
suffix: new Date().getHours() > 12 ? "PM" : "AM",
})
}
componentDidMount(){
this.intervalId = setInterval(
() => this.tick(), 1000
);
}
componentWillUnmount(){
clearInterval(this.intervalId);
}
handleClick(){
this.setState(state => ({update: !state.update}));
console.log("1",this.state.update);
if (this.state.update){
this.intervalId = setInterval(
() => this.tick(), 1000
);
console.log("2 set interval",this.intervalId);
}
else{
clearInterval(this.intervalId);
console.log("2 clear interval");
}
}
render() {
const { hh, mm, ss, suffix } = this.state;
return (
<div className="box" > London Clock
<div className="flexbox-container" onClick={() => this.handleClick()}>
<Content time={hh}></Content>
<div><p>:</p></div>
<Content time={mm}></Content>
<div><p>:</p></div>
<Content time={ss}></Content>
<div className="suffix"><p>{suffix}</p></div>
</div>
</div>
);
}
}
class Content extends React.Component {
state = {
editMode: false,
time: ""
};
componentDidMount(){
this.setState({
time: this.props.time,
})
}
handleKeyDown(event){
console.log(event,event.key === 'Enter');
if (event.key === 'Enter'){
this.setState({
editMode: false,
})
}
}
render() {
const {editMode} = this.state;
return (
<div>
{editMode? (
<p>
<input
defaultValue={this.props.time}
onKeyPress={e => this.handleKeyDown(e)}
/>
</p>
) : (
<p onClick={() => this.setState({ editMode: true })}>{this.props.time}</p>
)}
</div>
);
}
}
ReactDOM.render(
<Timer/>,
document.body
);
.flexbox-container {
display: flex;
flex-direction: row;
}
.suffix{
padding-left: 20px;
}
.box{
border-style: solid;
padding: 10px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

When your component is mounted, it will start an interval and assign it to intervalId.
Your clickhandler modifies the state and then immediately attemtps to look at the state, without waiting for it to be updated. The state likely is not updated at this point, so it reassigns the interval, resulting in a zombie-updater.
Either pass a callback along to setState(updater, [callback]) or move your interval-logic to componentDidUpdate, which would allow you to de-duplicate interval logic

Try like below instead of setting to state and bind your tick function.
componentDidMount(){
this.intervalId = setInterval(
() => this.tick.bind(this), 1000
);
}
componentWillUnmount(){
clearInterval(this.intervalId);
}

I think you can use React's Ref instead of the state.
constructor(props) {
this.timerRef = React.createRef();
}
componentDidMount() {
this.timerRef.current = setInterval(this.tick, 1000);
}
componentWillUnmount() {
clearInterval(this.timerRef.current);
}

Related

React Timer + Color Change

i have a simple div element in react, now i would like to be able to preset a time somewhere and for example after 10 minutes. The div would change color.
Does anyone know if that is possible to achieven?
Use the componentDidMount API to init timer, and don't forget to remove it at componentWillUnmount.
class App extends Component {
constructor() {
super()
this.state = {
color: 'blue'
}
}
handleChangeColor = (newColor) => {
this.setState({
color: newColor
})
}
componentDidMount() {
this.timer = setTimeout(
() => this.handleChangeColor('red'),
1000*3 // in milliseconds, 3s for fast show
)
}
componentWillUnmount() {
clearTimeout(this.timer)
}
render() {
return (
<div style={ { background: this.state.color} }>
Color Div
</div>
)
}
}
For full code, check here.

All the toasters close when clicked on the close button in react

I have made a toaster component of my own which on multiple clicks render multiple toasters. The problem I am facing is that all the toasters are terminated when the handle close component is clicked or when the settimeout function is called. I am passing messages through another component as props.
This is my toaster component
export default class MyToaster extends React.Component {
constructor(props) {
super(props);
this.state = {
message: props.message,
show: false,
no: 0
};
}
handleclose = () => {
this.setState({
show: false,
no: this.state.no - 1
})
}
handleOpen = () => {
console.log('HANDLE OPEN')
this.setState({
show: true,
no: this.state.no + 1
}, () => {
setTimeout(() => {
this.setState({
show: false,
no: this.state.no - 1
})
}, 3000)
})
}
createtoaster = () => {
if (this.state.show) {
let toastmessage = [];
for (let i = 0; i < this.state.no; i++) {
let tmessage = <div className="snackbar">
<div className="card-header">
<h3 className="card-title">Toast</h3>
</div>
<div className="card-body">
{this.state.message}
</div>
<div className="card-footer"></div>
<button className="btn" onClick={this.handleclose}>x</button>
</div>
toastmessage.push(tmessage);
}
return toastmessage;
} else {
return null;
}
};
render() {
return (
<div className="col-md-2 offset-md-9">
<button className="btn btn-primary" onClick={this.handleOpen}></button>
{this.createtoaster()}
</div>
)
}
}
I have tried managing the state in the parent component but it doesnt seem to work. I do know that the problem is in managing state of my toaster component but dont know the exact problem and the solution.
Any solutions for this also feel free to point out any of my mistakes.
TIA
Handle close is run on the click of any button rather on the instance of one of them by the looks of it.
if (this.state.show) { // this determines whether to render you toasts...
// and close turns all of them off.
You need to change each toast to have it's own show property and for close to toggle that one and remove it from the array of toasts to generate.
Note:
Your props and state should be separate, don't copy props into state as this will introduce bugs and changes will not be reflected.
constructor(props) {
super(props);
// avoid copying props into state
// https://reactjs.org/docs/react-component.html#constructor
this.state = {
message: props.message,
show: false,
no: 0
};
}
There is a different way to this approach.
export default class MyToaster extends React.Component {
constructor(props) {
super(props);
this.state = {
message: props.message,
show: true,
no: 0
};
}
componentDidMount() {
setTimeout(() => {
this.setState({show: false})
}, 4000)
}
handleclose = () => {
this.setState({
show: false,
no: this.state.no - 1
})
}
handleOpen = () => {
this.setState({
no: this.state.no + 1
}, () => {
setTimeout(() => {
this.setState({
show: false,
no: this.state.no - 1
})
}, 3000)
})
}
render() {
return (
<div className="col-md-2 offset-md-9">
{this.state.show
? (
<div className="container snackbar" style={this.props.style}>
<div className="card-header">
<h3 className="card-title">Toast</h3>
</div>
<div className="card-body">
{this.props.message}
</div>
<div className="card-footer"></div>
</div>
)
: null
}
</div>
)
}
}
And from your parent component you can include
this.state = {
toasterCollection: []
}
//make a function
handleToasterClick = () => {
const toaster = <Toaster message={this.message} style={this.style}/>
this.setState({
// toasterCollection: [...this.state.toasterCollection, toaster]
toasterCollection: [...this.state.toasterCollection, toaster]
});
}
//In your Render give a button
<button className="btn btn-primary" onClick={this.handleToasterClick}>
Toast
</button>
//Also render this
{this.state.toasterCollection}
This should get your code to work.

Change content in div based on logic using React

I'm failing to understand how to update the content inside my render function based on logic from inside of a function. Do I have to return a whole new render function in order to do so? If so, that seems counter intuitive with React's framework of state & props and such...
Here's what I've tried:
tick() {
this.setState(prevState => ({
minutes: prevState.seconds + 1,
}));
if(this.state.minutes > this.state.targetGoal){
console.log("NONONONONO");
return (<div>SOMETHING NEW</div>); //update content inside render()
}
}
async componentDidMount() {
this.interval = setInterval(() => this.tick(), 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
return (
<div style={divStyle}>
<div>Your Second Count: {this.state.seconds}</div>
<habitlab-logo-v2></habitlab-logo-v2>
<br/>
<close-tab-button></close-tab-button>
</div>
);
}
}
There are a few issues.
As #Jayce444 has pointed out, you need to change a state to trigger render to re-render.
So create a new flag (say isOvertime) to trigger the render to fire.
tick() {
this.setState(
prevState => ({
seconds: prevState.seconds + 1
}),
() => {
if (this.state.seconds > this.state.targetGoal) {
console.log("NONONONONO");
// return <div>SOMETHING NEW</div>; //update content inside render()
this.setState({ isOvertime: true });
}
}
);
}
And in the render, you show a component depending on the isOvertime.
render() {
const { isOvertime, seconds } = this.state;
return (
<div>
{isOvertime ? (
<div>Time Over Man!</div>
) : (
<div>Your Second Count: {seconds}</div>
)}
<input
type="number"
value={this.state.targetGoal}
onChange={e => this.setState({ targetGoal: e.target.value })}
/>
</div>
);
}
Here is the full source. (demo availabe on CodeSandBox).
Output
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
class App extends React.Component {
state = {
interval: 0,
seconds: 0,
targetGoal: 4,
isOvertime: false
};
tick() {
this.setState(
prevState => ({
seconds: prevState.seconds + 1
}),
() => {
if (this.state.seconds > this.state.targetGoal) {
console.log("NONONONONO");
// return <div>SOMETHING NEW</div>; //update content inside render()
this.setState({ isOvertime: true });
}
}
);
}
componentDidMount() {
const interval = setInterval(() => this.tick(), 1000);
this.setState({ interval });
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
const { isOvertime, seconds } = this.state;
return (
<div>
{isOvertime ? (
<div>Time Over Man!</div>
) : (
<div>Your Second Count: {seconds}</div>
)}
<input
type="number"
value={this.state.targetGoal}
onChange={e => this.setState({ targetGoal: e.target.value })}
/>
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Reactjs setState not updating for this one function only

For this application, clicking a listed item once should create a button component underneath this listed item. Clicking the button should cause this listed item to be deleted.
I am currently facing difficulty trying to 'delete' the listed item after the button is clicked. Here is the code that went wrong (this is found in CountdownApp component) :
handleDelete(index) {
console.log('in handleDelete')
console.log(index)
let countdownList = this.state.countdowns.slice()
countdownList.splice(index, 1)
console.log(countdownList) // countdownList array is correct
this.setState({
countdowns: countdownList
}, function() {
console.log('after setState')
console.log(this.state.countdowns) // this.state.countdowns does not match countdownList
console.log(countdownList) // countdownList array is still correct
})
}
In the code above, I removed the item to be deleted from countdownList array with splice and tried to re-render the app with setState. However, the new state countdowns do not reflect this change. In fact, it returns the unedited state.
I have also tried the following:
handleDelete(index) {
this.setState({
countdowns: [] // just using an empty array to check if setState still works
}, function() {
console.log('after setState')
console.log(this.state.countdowns)
})
}
In the code above, I tried setting state to be an empty array. The console log for this.state.countdowns did not print out an empty array. It printed out the unedited state again
This is the only event handler that isn't working and I have no idea why (main question of this post) :/
If I have 'setstate' wrongly, why does the other 'setState' in other parts of my code work?? (I would like to request an in-depth explanation)
This is all my code for this app (its a small app) below:
import React from 'react'
import ReactDOM from 'react-dom'
class DeleteButton extends React.Component {
render() {
return (
<ul>
<button onClick={this.props.onDelete}>
delete
</button>
</ul>
)
}
}
class Countdown extends React.Component {
render () {
//console.log(this.props)
return (
<li
onClick={this.props.onClick}
onDoubleClick={this.props.onDoubleClick}
>
{this.props.title} - {this.props.days}, {this.props.color}
{this.props.showDeleteButton ? <DeleteButton onDelete={this.props.onDelete}/> : null }
</li>
)
}
}
const calculateOffset = date => {
let countdown = new Date(date)
let today = new Date
let timeDiff = countdown.getTime() - today.getTime()
let diffDays = Math.ceil(timeDiff / (1000 * 3600 * 24))
return diffDays
}
class CountdownList extends React.Component {
countdowns() {
let props = this.props
// let onClick = this.props.onClick
// let onDoubleClick = this.props.onDoubleClick
let rows = []
this.props.countdowns.forEach(function(countdown, index) {
rows.push(
<Countdown
key={index}
title={countdown.title}
days={calculateOffset(countdown.date)}
color={countdown.color}
showDeleteButton={countdown.showDeleteButton}
onDelete={() => props.onDelete(index)}
onClick={() => props.onClick(index)}
onDoubleClick={() => props.onDoubleClick(index)}
/>
)
})
return rows
}
render() {
return (
<div>
<ul>
{this.countdowns()}
</ul>
</div>
)
}
}
class InputField extends React.Component {
render() {
return (
<input
type='text'
placeholder={this.props.placeholder}
value={this.props.input}
onChange={this.props.handleInput}
/>
)
}
}
class DatePicker extends React.Component {
render() {
return (
<input
type='date'
value={this.props.date}
onChange={this.props.handleDateInput}
/>
)
}
}
class CountdownForm extends React.Component {
constructor(props) {
super(props)
this.state = {
title: this.props.title || '',
date: this.props.date || '',
color: this.props.color || ''
}
}
componentWillReceiveProps(nextProps) {
this.setState({
title: nextProps.title || '',
date: nextProps.date || '',
color: nextProps.color || ''
})
}
handleSubmit(e) {
e.preventDefault()
this.props.onSubmit(this.state, this.reset())
}
reset() {
this.setState({
title: '',
date: '',
color: ''
})
}
handleTitleInput(e) {
this.setState({
title: e.target.value
})
}
handleDateInput(e) {
this.setState({
date: e.target.value
})
}
handleColorInput(e) {
this.setState({
color: e.target.value
})
}
render() {
return (
<form
onSubmit={(e) => this.handleSubmit(e)}
>
<h3>Countdown </h3>
<InputField
placeholder='title'
input={this.state.title}
handleInput={(e) => this.handleTitleInput(e)}
/>
<DatePicker
date={this.state.date}
handleDateInput={(e) => this.handleDateInput(e)}
/>
<InputField
placeholder='color'
input={this.state.color}
handleInput={(e) => this.handleColorInput(e)}
/>
<button type='submit'>Submit</button>
</form>
)
}
}
class CountdownApp extends React.Component {
constructor() {
super()
this.state = {
countdowns: [
{title: 'My Birthday', date: '2017-07-25', color: '#cddc39', showDeleteButton: false},
{title: 'Driving Practice', date: '2017-07-29', color: '#8bc34a', showDeleteButton: false},
{title: 'Korean BBQ', date: '2017-08-15', color: '#8bc34a', showDeleteButton: false}
]
}
}
handleCountdownForm(data) {
if (this.state.editId) {
const index = this.state.editId
let countdowns = this.state.countdowns.slice()
countdowns[index] = data
this.setState({
title: '',
date: '',
color: '',
editId: null,
countdowns
})
} else {
data.showDeleteButton = false
const history = this.state.countdowns.slice()
this.setState({
countdowns: history.concat(data),
})
}
}
handleDelete(index) {
console.log('in handleDelete')
console.log(index)
let countdownList = this.state.countdowns.slice()
countdownList.splice(index, 1)
console.log(countdownList)
this.setState({
countdowns: countdownList
}, function() {
console.log('after setState')
console.log(this.state.countdowns)
})
}
handleCountdown(index) {
const countdownList = this.state.countdowns.slice()
let countdown = countdownList[index]
countdown.showDeleteButton = !countdown.showDeleteButton
this.setState({
countdowns: countdownList
})
}
handleDblClick(index) {
const countdownList = this.state.countdowns
const countdown = countdownList[index]
this.setState({
title: countdown.title,
date: countdown.date,
color: countdown.color,
editId: index
})
}
render() {
return (
<div>
<CountdownForm
title={this.state.title}
date={this.state.date}
color={this.state.color}
onSubmit={(data) => {this.handleCountdownForm(data)}}
/>
<CountdownList
countdowns={this.state.countdowns}
onDelete={(index) => this.handleDelete(index)}
onClick={(index) => this.handleCountdown(index)}
onDoubleClick={(index) => this.handleDblClick(index)}
/>
</div>
)
}
}
ReactDOM.render(
<CountdownApp />,
document.getElementById('app')
)
I managed to find the answer to my own question!
setState worked as expected. The bug was due to <li> container that wrapped the event handler.
Clicking <li> causes it to call onClick event (which is managed by handleCountdown function in CountdownApp component) which causes it to setState.
As the delete button was wrapped in <li> container, clicking the delete button calls 2 event listeners - handleCountdown and handleDelete. handleCountdown is called twice in this case, once from clicking <li> to expand and the next call when the delete button is clicked.
There is a high chance that the last async setState dispatched from handleCountdown overwrites handleDelete's setState. Hence, the bug.
Here is changes: (I recoded everything again so the names might differ a little but the logic stays the same)
class Countdown extends React.Component {
render () {
return (
<li>
<div onClick={this.props.onClick} > // Add this div wrapper!
{this.props.title} - {this.props.days}, {this.props.color}
</div>
{this.props.toShow ?
<ButtonsGroup
onDelete={this.props.onDelete}
onEdit={this.props.onEdit}
/>
: null}
</li>
)
}
}
So the solution is to separate the clickable area and the buttons. I added a div wrapper over the text in <li> so whenever the text in <li> is clicked, the added <ul> will be out of onClick event handler area.

Element besides another

I am trying to display the time next to the text "Market Data" at the top header of the homepage. Tick is defined by the tick function below. The interval is set to one second.
The text "Market Data" gets displayed fine, but the time is not there.
var HomePage = React.createClass({
getInitialState: function() {
setInterval(this.tick, 1000);
},
tick : function() {
const element = (
<div>
<h1>{new Date().toLocaleTimeString()}.</h1>
</div>
},
render: function () {
return (
<div>
<div className="row">
<center>{this.tick}</center>
<center><p style={{ color:'blue', fontSize:'25px', fontWeight:'bold'}}>Market Data</p></center>
<StockTable stocks={this.state.stocks} last={this.state.last} />
</div>
</div>
);
}
});
React.render(<HomePage />, document.getElementById('main'));
I have similar code running. You need to trigger the component to update every tick, you'll do this by setting a new state after the timer. Setting a state triggers the component to update, but before you do, you remove the timer using componentWillUnmount(). When the updated component mounts, componentDidMount() will trigger and you set a new timer. It's something like an infinite cycle where each action triggers the next. Here's the code I'm using, it uses ES6 so you'll have to adapt it a bit.
constructor(props) {
super(props);
this.state = {
time: 0
};
}
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.state = { time:new Date() }
//The action you want to execute at every tick.
}
render(){
return(
<div>The time: {this.state.time}</div>
);
}
Here's the official explanation for this from React documentation. https://facebook.github.io/react/docs/state-and-lifecycle.html
Do this and you are good to go. The idea is that React updates the component when you change the state of the component. So, on certain interval, you are changing the state which leads to React rendering the change which is new time.
var HomePage = React.createClass({
componentWillMount: function(){
this.state = {
timeNow: new Date().toLocaleTimeString()
}
},
componentDidMount: function() {
this.timer = setInterval(this.tick.bind(this), 1000);
},
componentWillUnmount() {
clearInterval(this.timer);
},
tick : function() {
this.setState({
timeNow: new Date().toLocaleTimeString()
})
},
render: function () {
return (
<div>
<div className="row">
<center>
<div>
<h1>{this.state.timeNow}</h1>
</div>
</center>
<center><p style={{ color:'blue', fontSize:'25px', fontWeight:'bold'}}>Market Data</p></center>
</div>
</div>
);
}
});
React.render(<HomePage />, document.getElementById('main'));
Working example: https://jsfiddle.net/69z2wepo/73851/

Resources