I'm trying to implement timer that counts the next event from the JSON API. Unfortunately the API does not support filter so I have to process it on the component
here's the code:
constructor(props) {
super(props);
this.scrollViewRef = React.createRef();
this.state = {
agendaVisible: false,
scrollOffset: null,
markedDates: {},
dateString: Date(),
agendaItems: {},
nextEvent: {
start: 99999999999,
name: "",
},
isProcessing: true,
};
this.closeModal = this.closeModal.bind(this);
this.openModal = this.openModal.bind(this);
}
setMarkedDates = () => {
const { events } = this.props;
if (events !== undefined) {
Object.keys(events).map((key) => {
let now = Date.now();
let difference = events[key].start * 1000 - now;
let a = new Date(events[key].start * 1000);
let date = `${a.getUTCFullYear()}-${this.str_pad(
a.getUTCMonth() + 1
)}-${this.str_pad(a.getUTCDate())}`;
if (difference > 0 && difference < this.state.nextEvent.start) {
this.setState({
nextEvent: {
start: events[key].start * 1000,
name: events[key].name,
},
});
console.log("Goes in here: " + events[key].start);
}
}
// Set Marked dates on calendar and agenda Items
this.setState(({ markedDates, agendaItems }) => ({
markedDates: {
...markedDates,
[date]: {
marked: true,
selectedColor: "blue",
activeOpacity: 0,
dotColor: "blue",
},
},
agendaItems: {
...agendaItems,
[date]: [
{
name: events[key].name,
height: 100,
},
],
},
}));
});
this.setState({ isFetching: false });
}
};
Then I passed in the nextEvent to my EventTimer component in the render
{!this.state.isFetching && (
<EventTimer
nextEvent={this.state.nextEvent}
nextEventStart={this.state.nextEventStart}
/>
)}
Now the problem is, whenever I called {this.props.nextEvent.start} in the render, it works, however I need to set it up the state of eventTimer for static endTime to calculate the difference every interval, I put the code on componentWillReceiveProps but I never get the updated props? it still stays at 9999999999, what happened?
If it doesn't receive the props then which part of the lifecycle received the props? Because it works when I tried to render it. I'm not sure where should I update the state from the props anymore.
Please help, Thanks!
componentWillReceiveProps(nextProps) {
this.setState({
endTime: Date.now() + this.props.nextEvent.start,
});
this.resetTimer();
}
resetTimer = () => {
if (this.interval) clearInterval(this.interval);
this.interval = setInterval(
this.setTimeRemaining,
this.timeRemaining % 1000
);
};
You are referring to the initial props in the componentWillReciveProps.
this.setState({
// this.props refers to the current props.
// endTime: Date.now() + this.props.nextEvent.start,
endTime: Date.now() + nextProps.nextEvent.start,
});
While that should fix the bug you are facing. You should try to not use this lifecycle method and instead depend on a calculated value from the parent component.
The componentWillReciveProps will be deprecated in the next major version. You can read a bit more about this in the react docs.
Related
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 am new to react and decided to practice by implementing a simple stop watch using both class and functional components.
I successfully implemented the stop watch using a class component. Below is the code:
Class Component
class Stopwatch extends Component {
state = {
status: false,
ms: 0,
seconds: 0,
minutes: 0,
};
stopms;
stopSeconds;
stopMinutes;
handleClick = () => {
this.changeStatus();
if (this.state.status) {
clearInterval(this.stopms);
clearInterval(this.stopSeconds);
clearInterval(this.stopMinutes);
} else {
this.stopms = setInterval(this.changeMs, 1);
this.stopSeconds = setInterval(this.changeSeconds, 1000);
this.stopMinutes = setInterval(this.changeMinutes, 60000);
}
};
changeStatus = () => {
return this.setState((state) => {
return { status: !state.status };
});
};
changeMs = () => {
return this.setState((state) => {
if (state.ms === 99) {
return { ms: 0 };
} else {
return { ms: state.ms + 1 };
}
});
};
changeSeconds = () => {
return this.setState((state) => {
if (state.seconds === 59) {
return { seconds: 0 };
} else {
return { seconds: state.seconds + 1 };
}
});
};
changeMinutes = () => {
return this.setState((state) => {
if (state.seconds === 59) {
return { minutes: 0 };
} else {
return { minutes: state.minutes + 1 };
}
});
};
handleReset = () => {
clearInterval(this.stopms);
clearInterval(this.stopSeconds);
clearInterval(this.stopMinutes);
this.setState({ seconds: 0, status: false, minutes: 0, ms: 0 });
};
componentWillUnmount() {
clearInterval(this.stopms);
clearInterval(this.stopSeconds);
clearInterval(this.stopMinutes);
}
render() {
return (
<div>
<h1>
{this.state.minutes} : {this.state.seconds} .{" "}
<span>{this.state.ms}</span>
</h1>
<button className="btn btn-lg btn-dark" onClick={this.handleClick}>
{this.state.status === false ? "Start" : "Pause"}
</button>
<button className="btn btn-lg btn-dark" onClick={this.handleReset}>
Reset
</button>
</div>
);
}
}
export default Stopwatch;
Now I'm trying to implement the same code above but using a functional component as shown below:
Functional Component
function Stopwatch() {
const [timeState, setTimeState] = useState({
status: false,
ms: 0,
seconds: 0,
minutes: 0,
});
let stopms;
let stopSeconds;
let stopMinutes;
const handleClick = () => {
changeStatus();
if (timeState.status) {
clearInterval(stopms);
clearInterval(stopSeconds);
clearInterval(stopMinutes);
} else {
stopms = setInterval(changeMs, 1);
stopSeconds = setInterval(changeSeconds, 1000);
stopMinutes = setInterval(changeMinutes, 60000);
}
};
const changeStatus = () => {
return setTimeState((prevState) => {
return { ...prevState, status: !prevState.status };
});
};
const changeMs = () => {
return setTimeState((prevState) => {
if (prevState.ms === 99) {
return { ...prevState, ms: 0 };
} else {
return { ...prevState, ms: prevState.ms + 1 };
}
});
};
const changeSeconds = () => {
return setTimeState((prevState) => {
if (prevState.seconds === 59) {
return { ...prevState, seconds: 0 };
} else {
return { ...prevState, seconds: prevState.seconds + 1 };
}
});
};
const changeMinutes = () => {
return setTimeState((prevState) => {
if (prevState.seconds === 59) {
return { ...prevState, minutes: 0 };
} else {
return { ...prevState, minutes: prevState.minutes + 1 };
}
});
};
const handleReset = () => {
clearInterval(stopms);
clearInterval(stopSeconds);
clearInterval(stopMinutes);
setTimeState({ seconds: 0, status: false, minutes: 0, ms: 0 });
};
return (
<div>
<h1>
{timeState.minutes} : {timeState.seconds} . <span>{timeState.ms}</span>
</h1>
<button className="btn btn-lg btn-dark" onClick={handleClick}>
{timeState.status === false ? "Start" : "Stop"}
</button>
<button className="btn btn-lg btn-dark" onClick={handleReset}>
Reset
</button>
</div>
);
}
export default Stopwatch;
The Problem
In the class component, I implemented the "Pause" functionality using the handleClick function which calls clearInterval with it's argument as the global variables stopms, stopSeconds, stopMinutes that I declared initially. This worked just fine because these global variables were holding values returned from the respective setInterval when the stop watch started counting.
Now in the functional component, I replicated the same logic by declaring the same global variables using the "let" keyword. But the "Pause" functionality is not working. When the "Start" button hit and the handleClick function called, the setIntervals were called and their return values were stored in the respective global variables. But when the "Pause" button was hit, all the global variables had "undefined" as their values.
Please I would like to know if there's any other way I can declare global variables and use them to hold values throughout a component's life cycle asides using state.
Functional components are executed from top to bottom whenever state changes, so the whole function is re-executed and that's how it returns the new JSX, compare this to class components where only render() function is executed on render, that's how functional components work.
The problem is that your global variables are in fact not global and a part of the function, hence they are re-initialized each time render is happening.
Two ways to solve this
Move your variables to the state
function Stopwatch() {
const [timeState, setTimeState] = useState({
status: false,
ms: 0,
seconds: 0,
minutes: 0,
stopms : null,
stopSeconds : null,
stopMinutes: null,
});
const handleClick = () => {
changeStatus();
if (timeState.status) {
clearInterval(timeState.stopms);
clearInterval(timeState.stopSeconds);
clearInterval(timeState.stopMinutes);
} else {
let stopms = setInterval(changeMs, 1);
let stopSeconds = setInterval(changeSeconds, 1000);
let stopMinutes = setInterval(changeMinutes, 60000);
setTimeState(prev => ({..prev, stopms, stopSeconds, stopMinutes})); // update the values in state
}
};
......
const handleReset = () => {
clearInterval(timeState.stopms); // use the same values to clear them
clearInterval(timeState.stopSeconds);
clearInterval(timeState.stopMinutes);
.....
};
.....
}
Or make them global by placing them outside of your component, Will work but not recommended.
In your component file.
// declare them just above your function
let stopms;
let stopSeconds;
let stopMinutes;
function Stopwatch() {
const [timeState, setTimeState] = useState({
status: false,
ms: 0,
seconds: 0,
minutes: 0,
});
.....
const handleClick = () => {
changeStatus();
if (timeState.status) {
clearInterval(stopms);
clearInterval(stopSeconds);
clearInterval(stopMinutes);
} else {
stopms = setInterval(changeMs, 1);
stopSeconds = setInterval(changeSeconds, 1000);
stopMinutes = setInterval(changeMinutes, 60000);
}
.......
};
How to set the value of timerOn,timerTime``timerStart for the watch to start count from 120 seconds? const selectItem = this.props.items [1] -> second object in array items({seconds: 120}). At the moment the clock begins to count from 3 min 22 sec, and should be from 2 min
All code: https://stackblitz.com/edit/react-jcydop
Snippet:
const selectItem = this.props.items[1] //second 120
App
this.state = {
items: [
{
name: 'A',
description: 'Hello',
second: 70
},
{
name: 'B',
description: 'World',
second: 120
}
],
selectIndex: null
};
}
Watch
class Stopwatch extends Component {
constructor() {
super();
this.state = {
timerOn: false,
timerStart: 0,
timerTime: 0
};
}
componentDidMount() {
const selectItem = this.props.items[1]
this.setState({
timerOn: true,
timerTime: selectItem.second * 1000,
timerStart: Date.now() - this.state.timerTime
});
this.timer = setInterval(() => {
this.setState({
timerTime: Date.now() - selectItem.second * 1000
});
}, 10);
}
It sounds like you want to have a timer that starts at 2:00 (120 seconds) and starts counting up.
If that's the case, you can do it like this, initializing timerTime to 120 seconds (the value of props.items[1]) and every 10ms increment the time:
class Stopwatch extends Component {
constructor() {
super();
this.state = {
timerOn: false,
timerTime: 0
};
}
componentDidMount() {
// Note: you could move this block of
// code to the constructor if you want
const selectItem = this.props.items[1]
this.setState({
timerOn: true,
timerTime: selectItem.second * 1000,
});
this.timer = setInterval(() => {
this.setState(prevState => ({
timerTime: prevState.timerTime + 10
}));
}, 10);
}
Incrementing by 10ms every 10ms will increment once per second. If you want the stopwatch to go faster, increment by a higher number, 100 or 1000ms.
If I've misunderstood and you want the counter to count down, you should set state as timerTime: prevState.timerTime - 10
I'm not sure if you're using timerStart in some other place, but it doesn't seem to be needed here.
I'm building a sort of clock with React that has an option to increment or decrement a number (25 as default) in one component, and in another component it updates the timer (25:00 since we start at 25) to whatever the number is incremented or decremented to.
I have two components (Session and Clock) successfully performing their own actions, however I'm stumped as to how I can get the counter (Session component) to update the state of the timer in the Clock component. More specifically, I've been toying with this.props.minutes to no avail.
Question: How can I go about sharing the this.state.minutes property among components? Thank you in advance. I'm still a total beginner at React.
Session:
const Session = React.createClass({
getInitialState: function() {
return {
minutes: 25,
seconds: 0
};
},
increment: function() {
this.setState({ minutes: this.state.minutes + 1 });
},
decrement: function() {
this.setState({ minutes: this.state.minutes - 1 });
},
timeToString: function(time) {
return time + ':00';
},
render: function() {
return (
<section>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
{this.state.minutes}
<Clock/>
</section>
);
}
});
module.exports = Session;
Clock:
const Clock = React.createClass({
getInitialState: function() {
return { currentCount: 10 };
},
startTimer: function() {
var intervalId = setInterval(this.timer, 1000);
this.setState({ intervalId: intervalId });
},
pauseTimer: function() {
clearInterval(this.state.intervalId);
this.setState({ intervalId: this.state.currentCount });
},
timer: function() {
var newCount = this.state.currentCount - 1;
if (newCount >= 0) {
this.setState({ currentCount: newCount });
} else {
clearInterval(this.state.intervalId);
}
},
render: function() {
return (
<section>
<button onClick={this.startTimer}>Start</button>
<button onClick={this.pauseTimer}>Pause</button>
{this.state.currentCount}
</section>
);
}
});
module.exports = Clock;
You need to pass in the state from Session to Clock like so:
<Clock time={this.state.minutes} /> in your Session component
Then the 'state' is now available to your Clock component as this.props.time
or whatever you call it in the above code.
The moral of the story is that state passed down to from a parent component to a child component is done so using props
Relevant Docs:
https://facebook.github.io/react/docs/multiple-components.html
Edit: another key link in the docs:
https://facebook.github.io/react/tips/communicate-between-components.html
I don't know if I worded this right, so bear with me. Basically, I have a component that is a functioning counter (increments or decrements). The other component is a timer that counts down from (by default) 25 to 0.
Previously, I had the timer just set to the value of 25, but I am trying to have the timer change as the value of the counter changes, and when the use presses the "start" button, the timer will count down from whatever number was set by the counter.
I can get the components working individually, but not together.
I've tried setting this.state.currentCount to the value of this.props.time, and then changing the value of this.state.currentCount, but no luck. Either the timer doesn't budge or it doesn't reflect the value of the counter.
Not sure if I should be using componentWillReceiveProps instead.
Any help would be greatly appreciated. There's a screenshot at the bottom if that helps at all.
Session Component:
const Session = React.createClass({
getInitialState: function() {
return {
minutes: 25,
seconds: 0
};
},
increment: function() {
this.setState({ minutes: this.state.minutes + 1 });
},
decrement: function() {
this.setState({ minutes: this.state.minutes - 1 });
},
timeToString: function(time) {
return time + ':00';
},
render: function() {
return (
<section>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
{this.state.minutes}
<Clock time={this.state.minutes}/>
</section>
);
}
});
module.exports = Session;
Clock Component:
const Clock = React.createClass({
getInitialState: function() {
return { currentCount: this.props.time };
},
startTimer: function() {
var intervalId = setInterval(this.timer, 1000);
this.setState({ intervalId: intervalId });
},
pauseTimer: function() {
clearInterval(this.state.intervalId);
this.setState({ intervalId: this.props.time });
},
timer: function() {
var newCount = this.state.currentCount - 1;
if (newCount >= 0) {
this.setState({ currentCount: newCount });
} else {
clearInterval(this.state.intervalId);
}
},
render: function() {
return (
<section>
<button onClick={this.startTimer}>Start</button>
<button onClick={this.pauseTimer}>Pause</button>
{this.props.time}
<br></br>
{this.state.currentCount}
</section>
);
}
});
module.exports = Clock;
getInitialState only runs when the component is first initialized so on next
updates from the parent component it won't run that function. You are correct
in that you want to use one of the lifecycle events and in this case
componentWillReceiveProps sounds like the most appropriate because you can
setState there and you don't need to wait for the component to render (otherwise
you would use componentDidUpdate).
I haven't checked this code but I think it should work with this addition:
const Clock = React.createClass({
...
componentWillReceiveProps: function(nextProps) {
// Perhaps pause timer here as well?
this.setState({
currentCount: nextProps.time
})
},
...
});
because your timer depends on Start button. it would be good if you set state of currentCount in startTimer method.
startTimer: function() {
if(this.state.intervalId)
clearInterval(this.state.intervalId); //clear the running interval
this.setState({ currentCount: this.props.time }); // reset currentcount
var intervalId = setInterval(this.timer, 1000);
this.setState({ intervalId: intervalId });
},