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>
);
}
}
Related
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
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.
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
I've been trying to get the countDown() function to run automatically inside render() function, but can't seem to figure it out. Here's the code:
import React from 'react';
import ReactDOM from 'react-dom';
class Counter extends React.Component {
constructor(props) {
super(props);
this.countDown = this.countDown.bind(this);
this.state = {
count: 5,
message: ''
}
}
countDown() {
setInterval(() => {
if (this.state.count <= 0) {
clearInterval(this);
this.setState(() => {
return {message: "Click here to skip this ad"}
})
} else {
this.setState((prevState) => {
return {count: prevState.count - 1}
})
}
}, 1000)
}
render() {
return (
<div>
<h1 onLoad={this.countDown}>
{this.state.message ? this.state.message : this.state.count}
</h1>
</div>
)
}
}
ReactDOM.render(<Counter />, document.getElementById('app'));
I'm not even sure if this is the optimal way to do it. My goal was to have a 5-second countdown displayed on screen then replace it with the download message/link when the countdown hits zero.
Use componentDidMount for starting the interval and clear it (to be sure) in componentWillUnmount too.
Then use the this.setState correctly
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 5,
message: ''
}
}
componentDidMount() {
this.inter = setInterval(() => {
if (this.state.count <= 0) {
clearInterval(this.inter);
this.setState({
message: 'Click here to skip this ad'
});
} else {
this.setState((prevState) => ({count: prevState.count - 1}));
}
}, 1000);
}
componentWillUnmount() {
clearInterval(this.inter);
}
render() {
return (
<div>
<h1>
{this.state.message ? this.state.message : this.state.count}
</h1>
</div>
)
}
}
ReactDOM.render(<Counter />, document.getElementById('app'));
<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>
<div id="app"></div>
I would recommend calling countDown in componentDidMount also it is recommended to store the interval and clear it anyway in componentWillUnmount.
As is your countdown method will run indefinitely as you know is mostly the case with SetIntervals. Also try to avoid using onLoads to call event handlers. What you should do is make use of the component life cycle methods provided by React. Specifically ComponentDidMount() and ComponentDidUpdate() in your case.
For your countdown, try using something like this
class Clock extends React.Component {
state = {
counter: 10
}
//immediately is triggered. This only happens once. And we have it immediately call our countdown
componentDidMount() {
this.countDown()
}
countDown = () => {
this.setState((prevState) => {
return{
counter: prevState.counter - 1
}
})
}
//will get called everyt time we update the component state
componentDidUpdate(){
if(this.state.counter > 0){
setTimeout(this.countDown, 1000) //calls the function that updates our component state, thus creating a loop effect
}
}
render() {
return (
<div className="time">
The time is: {this.state.counter}
</div>
);
}
}
I have a Timer component in a returned from a TimerContainer
const Timer = ({ time = 0 }) => (
<div className={styles.timer}>{formatTime(time)}</div>
);
Timer.propTypes = {
time: PropTypes.number
};
class TimerContainer extends React.Component {
constructor(props) {
super(props);
this.state = {
secondsElapsed: 0
};
}
componentDidMount() {
this.interval = setInterval(this.tick.bind(this), 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
tick() {
this.setState({
secondsElapsed: this.state.secondsElapsed + 1
});
}
render() {
return <Timer time={this.state.secondsElapsed} />;
}
}
How do I get it to only start when I click on another Button component? In my main component I have two functions for the Buttons
handleEasyCards() {
this.setState(prevState => ({ currentCards: this.state.easyCards }));
}
handleHardCards() {
this.setState({ currentCards: this.state.hardCards });
}
render() {
return (
<div style={boardContainer}>
<div style={buttonContainer}>
<Button
difficulty={this.state.easyButton}
onClick={this.handleEasyCards}
/>
<Button
difficulty={this.state.hardButton}
onClick={this.handleHardCards}
/>
</div>
<Cards
cardTypes={this.state.currentCards}
removeMatches={this.removeMatches}
/>
</div>
);
}
}
I think I need to pass a callback to the Button component and call it in the handleHardCards and handleEasyCards. I don't think this is a conditional render because the Timer will start with either Button clicked.
You could have another variable in the state:
constructor(props) {
super(props);
this.state = {
secondsElapsed: 0,
isCountingTime: false,
};
}
Then change that variable when an event happen:
handleEasyCards() {
this.setState(prevState => ({
currentCards: this.state.easyCards,
isCountingTime: true,
}));
}
handleHardCards() {
this.setState({
currentCards: this.state.hardCards,
isCountingTime: true,
});
}
Until now Timer has not been mounted so have not started counting. But with isCountingTime set to true it will render and start counting:
<div style={boardContainer}>
{this.state.isCountingTime && <Timer />}
...
</div>
The good part is that you can "start" and "reset" Timer whenever you want just by changing isCountingTime variable to true. The bad part is that nothing is rendered (no default values) when is set to false.