The following is most likely a fundamental question. I have the following class. The value of this.isRunning is updated to true in start() but after running start() at stop() the value is still false. What is the reason for that? This happens when the class is imported into a React component and instantiated.
The latest version of Stopwatch as used in a React component can be seen below:
import moment from "moment";
export default class Stopwatch {
constructor() {
console.log("run");
this.isRunning = false;
this.lastUpdateTime = 0;
this.totalDuration = 0;
this.interval = null;
}
start() {
if (this.isRunning) {
return console.error("Stopwatch is already started");
}
console.log("Stopwatch started");
this.lastUpdateTime = window.performance.now();
this.isRunning = true;
console.log(this.isRunning);
this.interval = setInterval(() => {
this.totalDuration += window.performance.now() - this.lastUpdateTime;
this.lastUpdateTime = window.performance.now();
}, 100); // update about every 100 ms
}
stop() {
console.log(this.isRunning)
if (!this.isRunning) {
return console.error("Stopwatch is already stopped");
}
console.log("Stopwatch stopped");
console.log("Stopwatch duration:", this.getFormattedDuration());
this.isRunning = false;
clearInterval(this.interval);
}
reset() {
console.log("Stopwatch reset");
this.isRunning = false;
this.lastUpdateTime = 0;
this.totalDuration = 0;
clearInterval(this.interval);
}
formatTime = (ms) => moment(ms).format("mm:ss.S");
getFormattedDuration() {
if (this.lastUpdateTime === 0) {
return "00:00.000";
}
return this.formatTime(this.totalDuration);
}
}
First, the stopwatch is started. Then it is stopped, and finally, reset.
The console output is the following:
Here is a stripped-down version of the original React component where the issue still occurs:
import React from 'react';
import { Button, Row, Col } from 'antd';
import { I18n } from 'aws-amplify';
import Stopwatch from '../../../../../utils/Stopwatch';
export default function TenMeter(props) {
const stopwatch = new Stopwatch();
const startTrial = async (index) => {
stopwatch.start();
}
const completeTrial = async (index) => {
stopwatch.stop();
resetParameters();
}
const resetTrial = async (index) => {
resetParameters();
}
const resetTest = (index) => {
resetParameters();
}
/**
* Reset parameter values.
*/
const resetParameters = () => {
stopwatch.reset();
}
function getTrialsButtonGroup(index) {
return (
<Row key={index} gutter={[16, 16]}>
<Col xs={{ span: 24, offset: 0 }} lg={{ span: 24, offset: 0 }}>
<span>{I18n.get('Trial')} {index + 1}</span>
<Button style={{ marginLeft: "10px" }} onClick={() => startTrial(index)}>{I18n.get('Start')}</Button>
<Button style={{ marginLeft: "10px" }} onClick={() => resetTrial(index)}>{I18n.get('Reset')}</Button>
<Button style={{ marginLeft: "10px" }} onClick={() => completeTrial(index)}>{I18n.get('Stop')}</Button>
</Col>
</Row>
)
};
return (
<div>
{getTrialsButtonGroup(0)}
</div>
);
}
When used in the above React component, Stopwatch has several instances initialized as it can be seen that the constructor is run several times.
In this JSFiddle snippet the constructor runs only once but the same issue occurs when stopwatch.stop() is triggered using the console.
Related
Edit: This problem has been solved by adding a second parameter in the useEffect Hooks.
I am new to react and am building a toy react game. I borrowed this timer I found here. If user performs an action and the parent component re-renders, the timer stop counting down for a short period of time. According to (https://reactjs.org/docs/hooks-effect.html), react will remember the useEffect function, and call it later after performing the DOM updates. If that's the reason, are there any solutions or alternatives to this? If not, can anyone point me in the right direction? Any help will be appreciated!
Here is my code snippet for reference:
class Chalkboard extends Component {
constructor(props) {
super(props);
this.state = {
minutes: 1,
seconds: 0,
addSeconds: 0,
};
}
updateTimer() {
this.setState({ addSeconds: 0 });
}
handleTimer() {
window.removeEventListener('keydown', this.keyHandling);
this.setState({
gameOver: true,
});
}
render() {
return(
<Timer
initialMinutes={this.state.minutes}
initialSeconds={this.state.seconds}
onTimer={this.handleTimer}
addSeconds={this.state.addSeconds}
updateTimer={this.updateTimer}
/>
)
}
import React, { useState, useEffect } from 'react';
const Timer = (props) => {
const { initialMinutes = 0, initialSeconds = 0 } = props;
const [minutes, setMinutes] = useState(initialMinutes);
const [seconds, setSeconds] = useState(initialSeconds);
useEffect(() => {
let myInterval = setInterval(() => {
if (seconds > 0) {
setSeconds(seconds - 1);
}
else if (seconds === 0) {
if (minutes === 0) {
clearInterval(myInterval);
} else {
setSeconds(59);
setMinutes(minutes - 1);
}
}
}, 1000);
return () => {
clearInterval(myInterval);
};
});
useEffect(() =>{
if(props.addSeconds>0){
setSeconds(seconds + props.addSeconds);
props.updateTimer()
}
if(minutes === 0 && seconds === 0){
props.onTimer()
}
});
return (
<div className = "timer-container" >
{minutes === 0 && seconds === 0 ? null: (
<div>
{' '}
{minutes}:{seconds < 10 ? `0${seconds}` : seconds}
</div>
)}
</div>
);
};
export default Timer;
Can you provide an example on https://codesandbox.io/?
Try to pass [props, minutes, seconds] as a second parameter in useEffect hook.
https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect
Hi im trying to code a Timer. My problem is that the countdown should start after clicking a button and when that status is changed from start to stop and vise versa. I cant figure out how to handle it and which level to put it.
ADDITIONAL INFO:
When clicking the button, it goes to the method handler. Changes the status with setstate() but it renders at the end. Which is to late for the countdown to start.
Here is the Game Component:
import React, { Component } from 'react';
import './Game.css';
import Timer from './Timer'
class Game extends Component {
constructor() {
super();
}
state = {
buttonStatus: {status:"Start" , classButton:"Button ButtonBackgroundColorGrey" },
dotclass : "",
timer: 60
}
componentDidMount() {
this.timersignal();
}
buttonclick = () =>{
(this.state.buttonStatus.status === "Start")
? this.setState({buttonStatus:{status:"Stop",classButton:"Button ButtonBackgroundColorRed"},dotclass:"Dot"})
: this.setState({buttonStatus:{status:"Start",classButton:"Button ButtonBackgroundColorGrey"},dotclass:""})
this.componentDidMount();
}
timersignal = () => {
if(this.state.buttonStatus.status === "Stop") {
this.Interval = setInterval(() =>{
this.setState(() => ({
timer : this.state.timer - 1
}))
},1000)
console.log("didMount start")
}
else(
console.log("didMount stop")
)
}
render() {
return (
<div>
<div className="Body-Container">
<h2 className="States"> Time </h2>
<Timer buttonstate= {this.state.timer}/>
<button className={this.state.buttonStatus.classButton} onClick={this.buttonclick}>{this.state.buttonStatus.status}</button>
</div>
</div>
);
}
}
export default Game;
Here is the Timer Component:
import React, { Component } from 'react';
import "./Timer.css";
class Timer extends Component {
render() {
return (
<h3 className="Timer">{this.props.buttonstate}</h3>
);
}
}
export default Timer ;
You just need one method and call it in componentDidMount and on click.
timerToggle = () =>{
if((this.state.buttonStatus.status === "Start") {
this.setState({buttonStatus:{status:"Stop",classButton:"Button ButtonBackgroundColorRed"},dotclass:"Dot"})
clearInterval(this.Interval);
}else{
this.setState({buttonStatus:{status:"Start",classButton:"Button ButtonBackgroundColorGrey"},dotclass:""}) ;
this.Interval = setInterval(() =>{
this.setState(() => ({
timer : this.state.timer - 1
}))
},1000)
}
}
componentDidMount() {
this.timerToggle();
}
The Final Answer:
timerToggle = () =>{
if(this.state.buttonStatus.status === "Start") {
this.setState({buttonStatus:{status:"Stop",classButton:"Button ButtonBackgroundColorRed"},dotclass:"Dot"})
this.Interval = setInterval(() =>{
this.setState(() => ({
timer : this.state.timer - 1
}))
},1000)
}
else{
this.setState({buttonStatus:{status:"Start",classButton:"Button ButtonBackgroundColorGrey"},dotclass:""}) ;
clearInterval(this.Interval);
}
}
I don't really know why this issue is happening.
It is suppose to update 90 deg every second.
Sometimes it goes crazy and update more 90 degrees sometimes.
CodePen Link
const updateValue = _=>{
console.log(this.state.value + 90);
this.setState(s=>({
value: s.value + 90
}))
}
this.timerId = setInterval(updateValue, 1000)
"setInterval" and "setTimeOut" out of sync when tab is not focused
you must change your async doThis same as follows:
async doThis () {
const updateValue = _=>{
if (!document.hidden){
console.log(this.state.value + 90);
this.setState(s=>({value: s.value + 90}))
}
}
this.timerId = setInterval(updateValue, 1000)
}
I did this and I think your problem solved ... plase use this code:
const {useState} = React;
class App extends React.Component{
constructor(){
super();
this.state = {
value: 0
}
this.doThis = this.doThis.bind(this);
}
componentDidMount() {
this.doThis();
}
componentWillUnmount() {
clearInterval(this.timerId);
}
async doThis () {
const updateValue = _=>{
if (!document.hidden){
console.log(this.state.value + 90);
this.setState(s=>({value: s.value + 90}))
}
}
this.timerId = setInterval(updateValue, 1000)
}
render() {
return <Box value={this.state.value}/>;
}
}
function Box(props) {
const angle = props.value;
let style = {
transform: `rotateZ(${angle}deg)`,
transformOrigin: 'bottom'
}
let boxStyle = {
display: 'flex',
justifyContent: 'center'
}
return(
<div>
<div className='box' style={boxStyle}>
<div style={style} className="needle"></div>
</div>
</div>
)
}
ReactDOM.render(<App/>, document.getElementById('root'));
Every element of the array should be displayed for some time and the time for which each element is displayed should be determined by a value in each element.
let array=[{display:"a",time:10},{display:"b",time:15},{display:"c",time:22}]
class App extends React.Component{
state={stateDisplay:"",
stateTime:""
}
componentWillMount(){
var i=0;
let handle=setInterval(()=>{
var element= array[i]
this.setState({
stateDisplay:element.display,
stateTime:element.time,
})
i=i+1;
if(i===array.length){
clearInterval(handle)
}
},10000)
}
render(){
return(
<div> {this.state.stateDisplay} </div>
)}}
i have done something like this but using setinterval the delay can only be set for a constant time,here 10s.
I want the first element to display for 10s and then the next element for 15s, third for 22s which is the time value for each element of the array.
I know i cant do that using setinterval is there a way to do this using Settimeout?
This was almost like a little challenge, heres what i managed to come up with, its in typescript, if you need js, just remove interfaces and type annotations
/* eslint-disable #typescript-eslint/no-explicit-any */
/* eslint-disable prettier/prettier */
/* eslint-disable no-shadow */
/* eslint-disable no-console */
import React, { FC, useState, useEffect, useCallback } from 'react';
import { View, Button, Text } from 'react-native';
interface Data {
duration: number;
bgColor: string;
}
const dataArr: Data[] = [
{ duration: 3, bgColor: 'tomato' },
{ duration: 6, bgColor: 'skyblue' },
{ duration: 9, bgColor: 'gray' },
];
const Parent = () => {
const [currentIdx, setCurrentIdx] = useState<number>(0);
const [elementData, setElementData] = useState<Data>(dataArr[currentIdx]);
useEffect(() => {
console.log('idx', currentIdx);
if (currentIdx > dataArr.length) return;
setElementData({ ...dataArr[currentIdx] });
}, [currentIdx]);
const pushNext = () => {
setCurrentIdx(currentIdx + 1);
};
const handleRestart = () => {
setCurrentIdx(0);
setElementData({ ...dataArr[0] });
};
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Timer
data={elementData}
onCountDownComplete={pushNext}
restart={handleRestart}
/>
</View>
);
};
interface Props {
data: Data;
onCountDownComplete: () => void;
restart: () => void;
}
const Timer: FC<Props> = ({ data, onCountDownComplete, restart }) => {
const [seconds, setSeconds] = useState<number>(data.duration);
// update on data change
useEffect(() => {
setSeconds(data.duration);
}, [data]);
const callback = useCallback(() => {
onCountDownComplete();
}, [onCountDownComplete]);
useEffect(() => {
let interval: any = null;
if (seconds > -1) {
interval = setInterval(() => {
if (seconds - 1 === -1) {
callback();
} else {
setSeconds(seconds - 1);
}
}, 1000);
} else {
return;
}
return () => {
clearInterval(interval);
};
}, [seconds, callback]);
return (
<View
style={{ backgroundColor: data.bgColor, padding: 16, borderRadius: 10 }}
>
<Text style={{ marginBottom: 24 }}>{seconds}</Text>
<Button title="restart" onPress={restart} />
</View>
);
};
I have the following page in React:
#connect((store) => {
return {
all_orders_data: store.trends.all_orders_data,
...etc...
};
})
class OrdersController extends MyComponent {
constructor(props){
let custom_methods = ['refreshData', 'runDispatch'];
super(props, custom_methods);
this.state = {last_updated_at: new Date()};
}
componentWillMount() {
this.runDispatch();
// this.props.dispatch(fetchOrderData());
}
componentDidMount() {
this.refreshTimerID = setInterval(
() => this.refreshData(),
300 * 1000 // 5 minutes
);
}
refreshData() {
console.log('running refresh');
this.runDispatch(true);
this.setState({last_updated_at: new Date()});
}
runDispatch(refresh=false) {
this.props.dispatch(fetchOrderTrendingAllOrdersData(refresh));
}
render() {
return (
<div>
<h1>Order Trending</h1>
{last_updated_at}
<br/>
<div className="card col-md-12">
<h2 className="style-1">All Orders</h2>
<LineChart
data={all_orders_data}
fetched={fetched_all_orders_data}
error={error_in_fetching_all_orders_data}
/>
</div>
</div>
)
}
In the render I unpack the props, the last_updated header, and render a line chart.
I want to be able to click buttons to toggle lines on the chart. To do this, I have to keep track of the keys for the data variable to show as lines.
I can't put these line options in the constructor because the runDispatch isn't fired off until componentWillMount.
I can't put it in componentWillMount because I don't know when runDispatch will return for data until it does return.
I can get the keys for the all_orders_data in the reducer and pass it to OrdersController as a prop, but props can't be changed and I want this page of our app to control the current lines showing.
I don't want to put it on the chart component because it get's new props every time to refresh runs, and I don't know if it will maintain the proper state after refresh.
The toggle setting doesn't need to be retained later, only while the controller is active (if they pick a new link I don't care if it resets).
My gut is to put state on the line chart since it doesn't have to be permanent.
Where is the correct place to keep the state of these line keys, like:
{
all_orders: ['Online orders', 'Web orders', ...]
}
to let the user toggle what lines he wants to see on graph? It can be on the LineChart, the controller, a new redux prop, etc.
I decided to put it on the LineChart itself, since the buttons toggle lines on it. It wasn't working, but you can protect the state in componentWillReceiveProps:
import React from 'react';
import ReactLoading from 'react-loading';
import {
VictoryChart,
VictoryLine,
VictoryTheme,
VictoryAxis,
VictoryTooltip,
VictoryBar,
VictoryVoronoiContainer
} from 'victory';
import _ from 'underscore';
import MyComponent from '../main/MyComponent';
const COLOR_OPTIONS = [
'#c43a31', // dark red
'blue',
'green',
'yellow',
'purple',
'teal',
'orange'
];
function getTimestringFromUnixTimestamp(timestamp) {
// just default it to AM for now
let period = 'AM'
let date = new Date(timestamp);
let hours = date.getHours();
let minutes = date.getMinutes();
if (hours >= 12) {
period = 'PM';
}
if (hours == 0) {
hours += 12;
} else if (hours >= 13) {
hours -= 12;
}
hours = "0" + hours;
minutes = "0" + minutes;
// Will display time in 10:30 AM format
let formattedTime = `${hours.substr(-2)}:${minutes.substr(-2)} ${period}`;
return formattedTime
}
function displayTooltips(data) {
// x is the unix timestamp, y is the order count
let { x, y } = data;
let formattedTime = getTimestringFromUnixTimestamp(x);
// https://stackoverflow.com/questions/847185/convert-a-unix-timestamp-to-time-in-javascript
return `Time - ${formattedTime}\nOrder Count - ${y}`
}
export default class LineChart extends MyComponent {
constructor(props) {
let custom_methods = [
'generateVictoryLines',
'generateToggleButtons',
'toggleItemOnclick',
'toggleAllLines',
];
super(props, custom_methods);
this.state = {
active_line_keys: [],
available_line_keys: []
};
}
componentWillReceiveProps(nextProps) {
console.log('\n\ncomponentWillReceiveProps:');
let data = nextProps.data;
console.log(data);
if (data) {
let line_keys = Object.keys(data);
console.log('line_keys:');
console.log(line_keys);
console.log('this.state.available_line_keys:');
console.log( this.state.available_line_keys);
let is_equal = _.isEqual(_.sortBy(line_keys), _.sortBy(this.state.available_line_keys));
if (!is_equal) {
console.log('line keys are diff; need to update state');
this.setState({
available_line_keys: line_keys,
active_line_keys: line_keys
});
}
}
}
generateVictoryLines() {
return this.state.active_line_keys.map((key, index) => {
let this_keys_permanent_index = this.state.available_line_keys.indexOf(key);
let color = COLOR_OPTIONS[this_keys_permanent_index];
return (
<VictoryLine
labels={displayTooltips}
labelComponent={<VictoryTooltip/>}
style={{
data: { stroke: `${color}` },
parent: { border: `1px solid #ccc`}
}}
data={this.props.data[key]}
/>
)
});
}
generateToggleButtons() {
return this.state.available_line_keys.map((key, index) => {
let this_keys_permanent_index = this.state.available_line_keys.indexOf(key);
let color = COLOR_OPTIONS[this_keys_permanent_index];
console.log(key);
return (
<button onClick={this.toggleItemOnclick.bind(null, key)} style={ {color: color}}>{key}</button>
);
})
}
toggleItemOnclick(name) {
console.log('\ntoggleItemOnclick:');
console.log(name);
console.log(this.state);
let is_in_active_line_keys = this.state.active_line_keys.indexOf(name) != -1;
console.log(is_in_active_line_keys);
let new_active_line_keys;
if (is_in_active_line_keys) {
new_active_line_keys = this.state.active_line_keys.filter(e => e !== name); // e is each item in the list; filter
// this.setState({active_line_keys: new_active_line_keys});
} else {
new_active_line_keys = this.state.active_line_keys.slice();
new_active_line_keys.push(name);
}
console.log(new_active_line_keys);
this.setState({active_line_keys: new_active_line_keys});
// arr = arr.filter(e => e !== el);
}
toggleAllLines() {
if (this.state.active_line_keys.length < this.state.available_line_keys.length) {
this.setState({active_line_keys: this.state.available_line_keys});
} else {
this.setState({active_line_keys: []});
}
}
// addAllLines() {
// this.setState({active_line_keys: this.state.available_line_keys});
// }
render() {
let graph_body;
let { data, error, fetched } = this.props; // we don't need data here
let victory_lines = this.generateVictoryLines();
let buttons = this.generateToggleButtons();
if (error) {
// alert(error);
console.log('\n\nerror:');
console.log(error);
graph_body = (<h2 className="centered">Error retrieving data</h2>);
} else if (fetched == false) {
graph_body = (
<div className="card col-md-12">
<span><h2 style={{fontWeight: "bold", fontSize: "25px", color: "#F96302"}}>Just a moment...Your request is being processed.</h2>
<ReactLoading type="cylon" color="#F96302" /></span>
</div>
)
} else {
try {
// in the victoryLine, interpolation="natural" will smooth graph out
graph_body = (
<div>
<VictoryChart
theme={VictoryTheme.material}
scale={{x: "time", y: "linear"}}
animate={{duration: 650}}
containerComponent={<VictoryVoronoiContainer/>}
width={1500}
height={600}
>
<VictoryAxis style={ { tickLabels: {fontSize: '20px'} } }/>
<VictoryAxis
dependentAxis
tickValues={[20, 40, 60, 80, 100]}
style={ { tickLabels: {fontSize: '20px'} } }
/>
{victory_lines}
</VictoryChart>
<button onClick={this.toggleAllLines}>Toggle lines</button>
{buttons}
</div>
)
} catch(err) {
graph_body = (<h2 className="centered">Error using response: {`${err}`}</h2>);
}
}
return (
<div>
{graph_body}
</div>
)
}
}