React setState in setInterval not updated immediately - javascript

I was working on a slideshow component that changing its interval for auto-playing. When I click speed up or slow down, the state is using the value in one update before, not the one currently updated, even I used setState().
Edit:
For a detailed explanation of value not immediately updated and the neediness of using callback in setState(),
see this useful post When to use React setState callback
var id;
const data = [img1, img2, img3, img4];
class Slideshow extends React.Component {
constructor(props) {
super(props);
this.state = { ImgId: 0, interval: 2000 };
}
startSlideshow() {
this.pause(); // prevent start multiple setInterval()
id = setInterval(() => {
this.setState(state => ({...state, ImgId: (state.ImgId + 1) % data.length}))
}, this.state.interval);
console.log(this.state.interval);
}
pause() {
if (id) {
clearInterval(id);
}
}
slowDown() {
this.setState((state) => ({...state, interval: state.interval + 250}));
this.startSlideshow();
}
speedUp() {
this.setState((state) => ({...state, interval: state.interval === 250 ? 250 : state.interval - 250}));
this.startSlideshow();
}
render() {
return (
<>
<button onClick={() => this.startSlideshow()}>Start</button>
<button onClick={() => this.pause()}>Pause</button>
<button onClick={() => this.slowDown()}>Slow down</button>
<button onClick={() => this.speedUp()}>Speed up</button>
<img src={"images/"+data[this.state.ImgId].filename} className="w-100"/>
<h6 className="text-center">{data[this.state.ImgId].filename}</h6>
</>
);
}
}

Use like :
slowDown() {
this.setState((state) => ({...state, interval: state.interval + 250}), ()=>{
this.startSlideshow();
);
}
speedUp() {
this.setState((state) => ({...state, interval: state.interval === 250 ? 250 : state.interval - 250}), ()=>{
this.startSlideshow();
);
}
setState have a callback, trigger after setting complete

Related

Why is the timer not working correctly when updating data in React?

My timer (MODAL -> componentDidUpdate -> timer) starts to work incorrectly after updating the data (MAIN -> componentDidMount -> timer) - it starts working faster and outputs -1 at the end. What's wrong? If you remove the data update (MAIN -> componentDidMount -> timer), the timer (MODAL -> componentDidUpdate -> timer) works correctly.
import React, { Component } from 'react';
class Main extends Component {
constructor(props) {
super(props);
this.state = {
data: [],
isOpened: false
};
this.submitForm = this.submitForm.bind(this);
}
componentWillMount() {
fetch('/api/global.json')
.then(response => response.json())
.then(result =>
this.setState({
data: result.data
// get data
}));
}
componentDidMount() {
this.timer = setInterval(() => this.componentWillMount(), 10000);
}
componentWillUnmount() {
this.timer = null;
}
changeModal = () => {
this.setState({ isOpened: !this.state.isOpened });
}
submitForm(e) {
e.preventDefault();
this.setState({ isOpened: !this.state.isOpened });
}
render() {
return (
<div>
<form onSubmit={this.submitForm}>
<button className="button button-main" type="submit">Modal open</button>
</form>
<Modal isOpened={this.state.isOpened} changeModal={() => this.changeModal(this)} />
</div>
);
}
}
class Modal extends Component {
constructor(props) {
super(props);
this.state = { counter: 30 };
}
componentDidUpdate(prevProps) {
if (prevProps.isOpened !== this.props.isOpened) {
if (!this.props.isOpened) {
this.setState({ counter: 30 });
clearTimeout(this.timer);
}
}
if (this.props.isOpened && this.state.counter > 0) {
this.timer = setTimeout(() => this.setState({ counter: this.state.counter - 1 }), 1000);
}
}
componentWillUnmount() {
clearTimeout(this.timer);
}
render() {
const padTime = time => {
return String(time).length === 1 ? `0${time}` : `${time}`;
};
const format = time => {
const minutes = Math.floor(time / 60);
const seconds = time % 60;
return `${minutes} мин ${padTime(seconds)} сек`;
};
return (
<div className={"modal-overlay" + (this.props.isOpened ? " open" : " close")}>
<div className='modal-content'>
<div className='modal-close' onClick={() => this.props.changeModal()}>×</div>
<div className="modal-note">{this.state.counter === 0 ? 'Finish!' : 'Timer ' + format(this.state.counter)}</div>
</div>
</div>
)
}
}
export default Main;
While I'm waiting for the best solution, I did the following and it works - the counter in the modal window does not speed up after updating the main component. But I'm not sure if this is the correct solution. I look forward to your help.
class Modal extends Component {
constructor(props) {
super(props);
this.state = { counter: 30 };
}
componentDidMount() {
if (this.state.counter > 0) {
this.timer = setInterval(() => this.setState({ counter: this.state.counter > 0 ? this.state.counter - 1 : 0}), 1000);
}
}
componentDidUpdate(prevProps) {
if (prevProps.isOpened !== this.props.isOpened) {
if (this.props.isOpened) {
this.setState({ counter: 30 });
}
}
}
componentWillUnmount() {
clearInterval(this.timer);
}
render() {
const padTime = time => {
return String(time).length === 1 ? `0${time}` : `${time}`;
};
const format = time => {
const minutes = Math.floor(time / 60);
const seconds = time % 60;
return `${minutes} мин ${padTime(seconds)} сек`;
};
return (
<div className={"modal-overlay" + (this.props.isOpened ? " open" : " close")}>
<div className='modal-content'>
<div className='modal-close' onClick={() => this.props.changeModal()}>×</div>
<div className="modal-note">{this.state.counter === 0 ? 'Finish!' : 'Timer ' + format(this.state.counter)}</div>
</div>
</div>
)
}
}

React change className in x second if a specific className is active

I just started learning React and I'm trying to create a simple reaction time app. I got stuck a little and I don’t know how to solve it. I could solve it to change the className on click, but I'd like to add a function that runs only if the "game-area-off" is active and it should change the classname to "game-area-on" at random times between 3-6 seconds.
So far i have come up with the code:
import "./App.css";
import React from "react";
class App extends React.Component {
constructor(props) {
super(props);
this.state = { isToggleOn: true, gameClass: "game-area-start" };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState((state) => ({
isToggleOn: !state.isToggleOn,
gameClass: state.isToggleOn ? "game-area-off" : "game-area-start",
}));
}
render() {
return (
<div className={this.state.gameClass} onClick={this.handleClick}>
<h1 className="default-text">
{this.state.isToggleOn ? "Click anywhere to start" : "Wait for green"}
</h1>
</div>
);
}
}
export default App;
In the handleClick callback you can use a timeout with a random delay to toggle the "game-area-on" state.
const randomDelay = Math.random() * 3000 + 3000;
setTimeout(() => {
this.setState({
// set go state
});
}, randomDelay);
I suggest using two pieces of state, 1 to track when the app is waiting to display the "on" value, and the second to display it. Additionally, the classname can be derived from the state, so it doesn't really belong there since it's easily computed in the render method.
Here is suggested code:
class ReactionTime extends React.Component {
state = {
gameRunning: false,
isToggleOn: false,
startTime: 0,
endTime: 0
};
// timeout refs
clearOutTimer = null;
gameStartTime = null;
componentWillUnmount() {
// clear any running timeouts when the component unmounts
clearTimeout(this.clearOutTimer);
clearTimeout(this.gameStartTime);
}
handleClick = () => {
if (this.state.gameRunning) {
// handle end click
if (this.state.isToggleOn) {
clearTimeout(this.clearOutTimer);
this.setState({
gameRunning: false,
isToggleOn: false,
endTime: performance.now()
});
}
} else {
// handle start click - reaction "game" started
this.setState((prevState) => ({
gameRunning: true,
isToggleOn: false,
startTime: 0,
endTime: 0
}));
// set timeout to display "on"
const randomDelay = Math.random() * 3000 + 3000;
setTimeout(() => {
this.setState({
isToggleOn: true,
startTime: performance.now()
});
}, randomDelay);
// reaction "game" timeout to reset if user is too slow
this.clearOutTimer = setTimeout(() => {
this.setState({
gameRunning: false,
isToggleOn: false,
startTime: 0,
endTime: 0
});
}, 10000); // 10 seconds
}
};
render() {
const { gameRunning, isToggleOn, startTime, endTime } = this.state;
const className = gameRunning
? isToggleOn
? "game-area-on"
: "game-area-off"
: "game-area-start";
const reactionTime = Number(endTime - startTime).toFixed(3);
return (
<div className={className} onClick={this.handleClick}>
<h1 className="default-text">
{gameRunning
? isToggleOn
? "Go!"
: "Wait for green"
: "Click anywhere to start"}
</h1>
{reactionTime > 0 && <div>Reaction Time: {reactionTime}ms</div>}
</div>
);
}
}

I have a stopwatch in reactjs, how can I add each number into some sort of array to show each number?

I created a stopwatch using react. My stopwatch starts from 0 and stops at the press of the space button with componenDidMount and componentWillMount. My issue is, I can't seem to figure out how to create some sort of list with the numbers the stopwatch returns. I've created:
times = () => {
this.setState(previousState => ({
myArray: [...previousState.myArray, this.state.milliSecondsElapsed]
}));
};
and then in render() to print it.
<h1>{this.times}</h1>
What I'm trying to do is to create some sort of array that'll keep track of milliSecondsElapsed in my handleStart and handleStop method.
Here's what I have.
import React, {Component} from "react";
import Layout from '../components/MyLayout.js';
export default class Timer extends React.Component {
constructor(props) {
super(props);
this.state = {
milliSecondsElapsed: 0,
timerInProgress: false // state to detect whether timer has started
};
this.updateState = this.updateState.bind(this);
this.textInput = React.createRef();
}
componentDidMount() {
window.addEventListener("keypress", this.keyPress);
}
componentWillUnmount() {
window.removeEventListener("keypress", this.keyPress);
}
textInput = () => {
clearInterval(this.timer);
};
updateState(e) {
this.setState({})
this.setState({ milliSecondsElapsed: e.target.milliSecondsElapsed });
}
keyPress = (e) => {
if (e.keyCode === 32) {
// some logic to assess stop/start of timer
if (this.state.milliSecondsElapsed === 0) {
this.startBtn.click();
} else if (this.state.timerInProgress === false) {
this.startBtn.click();
} else {
this.stopBtn.click();
}
}
};
handleStart = () => {
if (this.state.timerInProgress === true) return;
this.setState({
milliSecondsElapsed: 0
});
this.timer = setInterval(() => {
this.setState(
{
milliSecondsElapsed: this.state.milliSecondsElapsed + 1,
timerInProgress: true
},
() => {
this.stopBtn.focus();
}
);
}, 10);
};
handleStop = () => {
this.setState(
{
timerInProgress: false
},
() => {
clearInterval(this.timer);
this.startBtn.focus();
}
);
};
times = () => {
this.setState(previousState => ({
myArray: [...previousState.myArray, this.state.milliSecondsElapsed]
}));
};
render() {
return (
<Layout>
<div className="index" align='center'>
<input
value={this.state.milliSecondsElapsed/100}
onChange={this.updateState}
ref={this.textInput}
readOnly={true}
/>
<button onClick={this.handleStart} ref={(ref) => (this.startBtn = ref)}>
START
</button>
<button onClick={this.handleStop} ref={(ref) => (this.stopBtn = ref)}>
STOP
</button>
<h1>{this.state.milliSecondsElapsed/100}</h1>
</div>
</Layout>
);
}
}
Issue
this.times is a function that only updates state, it doesn't return any renderable JSX.
times = () => {
this.setState((previousState) => ({
myArray: [...previousState.myArray, this.state.milliSecondsElapsed]
}));
};
Solution
Create a myArray state.
this.state = {
myArray: [], // <-- add initial empty array
milliSecondsElapsed: 0,
timerInProgress: false // state to detect whether timer has started
};
Move the state update logic from this.times to this.handleStop.
handleStop = () => {
this.setState(
(previousState) => ({
timerInProgress: false,
myArray: [
...previousState.myArray, // <-- shallow copy existing data
this.state.milliSecondsElapsed / 100 // <-- add new time
]
}),
() => {
clearInterval(this.timer);
this.startBtn.focus();
}
);
};
Render the array of elapsed times as a comma separated list.
<div>{this.state.myArray.join(", ")}</div>
Full code
class Timer extends React.Component {
constructor(props) {
super(props);
this.state = {
myArray: [],
milliSecondsElapsed: 0,
timerInProgress: false // state to detect whether timer has started
};
this.updateState = this.updateState.bind(this);
this.textInput = React.createRef();
}
componentDidMount() {
window.addEventListener("keypress", this.keyPress);
}
componentWillUnmount() {
window.removeEventListener("keypress", this.keyPress);
}
textInput = () => {
clearInterval(this.timer);
};
updateState(e) {
this.setState({ milliSecondsElapsed: e.target.milliSecondsElapsed });
}
keyPress = (e) => {
if (e.keyCode === 32) {
// some logic to assess stop/start of timer
if (this.state.milliSecondsElapsed === 0) {
this.startBtn.click();
} else if (this.state.timerInProgress === false) {
this.startBtn.click();
} else {
this.stopBtn.click();
}
}
};
handleStart = () => {
if (this.state.timerInProgress === true) return;
this.setState({
milliSecondsElapsed: 0
});
this.timer = setInterval(() => {
this.setState(
{
milliSecondsElapsed: this.state.milliSecondsElapsed + 1,
timerInProgress: true
},
() => {
this.stopBtn.focus();
}
);
}, 10);
};
handleStop = () => {
this.setState(
(previousState) => ({
timerInProgress: false,
myArray: [
...previousState.myArray,
this.state.milliSecondsElapsed / 100
]
}),
() => {
clearInterval(this.timer);
this.startBtn.focus();
}
);
};
render() {
return (
<div>
<div className="index" align="center">
<input
value={this.state.milliSecondsElapsed / 100}
onChange={this.updateState}
ref={this.textInput}
readOnly={true}
/>
<button
onClick={this.handleStart}
ref={(ref) => (this.startBtn = ref)}
>
START
</button>
<button onClick={this.handleStop} ref={(ref) => (this.stopBtn = ref)}>
STOP
</button>
<h1>{this.state.milliSecondsElapsed / 100}</h1>
</div>
<div>{this.state.myArray.join(", ")}</div>
</div>
);
}
}

Calling the 'stop' function that stops the clock by clicking <li> </ li>. Passing functions 'stop' between components

Expected effect:
Click Name A. Name A is active
When I click on the 'Name B' element, I want to call the 'stopTimer ()' function. Stop the time in the element 'Name A' and run function 'stopTimer()'
When I click an element such as 'Name A', name A is active, I can not call the 'stopTimer ()' function until I click 'Name B'
And vice versa when item 'Name B' is active. Click item 'Name A' call function 'stopTimer ()'
Is this solution possible at all? I am asking for advice.
Updated:
All code: https://stackblitz.com/edit/react-kxuvgn
I understood my mistake. I should put the function stopTimer () in the parent App. The function stopTimer is needed both in theApp, when I click to stop the clock, as well as in the Stopwatch component plugged into thestop button. Where should I set this: {timerOn: false}); clearInterval (this.timer);That it was common for both components?
stopTimer = () => {
     this.setState ({timerOn: false});
     clearInterval (this.timer);
};
App
class App extends React.Component {
constructor() {
super();
this.state = {
selectedTodoId: '',
selectedTabId: null,
items: [
{
id: 1,
name: 'A',
description: 'Hello'
},
{
id: 2,
name: 'B',
description: 'World'
}
],
selectIndex: null
};
}
stopTimer = (timer, timerOn) => {
this.setState({ timerOn: timerOn });
clearInterval(timer);
};
select = (id) => {
if(id !== this.state.selectedTabId){
this.setState({
selectedTodoId: id,
selectedTabId: id
})
this.stopTimer();
}
}
isActive = (id) => {
return this.state.selectedTabId === id;
}
render() {
return (
<div>
<ul>
{
this.state.items
.map((item, index) =>
<Item
key={index}
index={index}
item={item}
select={this.select}
items = {this.state.items}
selectIndex = {this.state.selectIndex}
isActive= {this.isActive(item.id)}
/>
)
}
</ul>
<ItemDetails
items = {this.state.items}
selectIndex = {this.state.selectIndex}
resul={this.state.resul}
/>
<Stopwatch
stopTimer = {this.stopTimer}
/>
</div>
);
}
}
Watch
class Stopwatch extends Component {
constructor() {
super();
this.state = {
timerOn: false,
timerStart: 0,
timerTime: 0
};
}
startTimer = () => {
this.setState({
timerOn: true,
timerTime: this.state.timerTime,
timerStart: Date.now() - this.state.timerTime
});
this.timer = setInterval(() => {
this.setState({
timerTime: Date.now() - this.state.timerStart
});
}, 10);
};
stopTimer = () => {
this.setState({ timerOn: false });
clearInterval(this.timer);
};
resetTimer = () => {
this.setState({
timerStart: 0,
timerTime: 0
});
};
render() {
const { timerTime } = this.state;
let centiseconds = ("0" + (Math.floor(timerTime / 10) % 100)).slice(-2);
let seconds = ("0" + (Math.floor(timerTime / 1000) % 60)).slice(-2);
let minutes = ("0" + (Math.floor(timerTime / 60000) % 60)).slice(-2);
let hours = ("0" + Math.floor(timerTime / 3600000)).slice(-2);
return (
<div>
<div className="Stopwatch-display">
{hours} : {minutes} : {seconds}
</div>
{ (
<button onClick={this.startTimer}>Start</button>
)}
{(
<button onClick={this.stopTimer}>Stop</button>
)}
{this.state.timerOn === false && this.state.timerTime > 0 && (
<button onClick={this.resetTimer}>Reset</button>
)}
</div>
);
}
}
You have to lift your state up for this, before the child unmounts, pass the value to
parent component to store.
componentWillUnmount() {
this.stopTimer();
this.props.updateTimerTime(this.props.index, this.state.timerTime);
}
When the child component mounts set the state from props passed from parent component.
componentDidMount() {
this.setState({
timerTime: this.props.timerTime,
});
}
Stackblitz demo

setTimeout in react setState

this.setState(prevState => ({
score: prevState.score + 10,
rightAnswers: prevState.rightAnswers + 1,
currentQuestion: setTimeout(() => {
prevState.currentQuestion + 1
}, 2000)
}))
}
On button click I change the state. My goal is to have a delay in currentQuestion state change, during which I want to show certain status messages, yet I want to update the score right away without delays.
What's the proper way to do that?
PS: This variant doesn't work, it's for the overall representation of what I want to do.
Thanks.
You can do this multiple ways:
1) Make two calls to setState. React will batch any concurrent calls to setState into one batch update, so something like this is perfectly fine:
this.setState( prevState => ({
score: prevState.score + 10,
rightAnswers: prevState.rightAnswers + 1
}));
setTimeout( () => {
this.setState( prevState => ({
currentQuestion: prevState.currentQuestion + 1
}));
}, 2000);
2) You can use the setState callback to update state after your first call is finished:
this.setState(prevState => ({
score: prevState.score + 10,
rightAnswers: prevState.rightAnswers + 1
}), () => {
setTimeout( () => {
this.setState( prevState => ({
currentQuestion: prevState.currentQuestion + 1
}));
}, 2000);
});
First use setState to change score and question with some value like null so that you know its updating and then also set timeout after that.
class Example extends React.Component {
constructor(props) {
super(props)
this.state = {
score: 1,
question: "A"
}
}
update() {
this.setState(prev => ({
score: prev.score + 1,
question: null
}));
this.change = setTimeout(() => {
this.setState({question: "B"})
}, 2000)
}
render() {
let {score, question} = this.state;
let style = {border: "1px solid black"}
return (
<div style={style} onClick={this.update.bind(this)}>
<div>{score}</div>
<div>{question ? question : "Loading..."}</div>
</div>
)
}
}
ReactDOM.render( < Example / > , document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>

Categories