Set a single setTimeout in React - javascript

I'm building a multiplication testing app, and I want each question to have an 8-second countdown that resets when an answer is ended. My problem is that when my component re-renders another instance of the setTimeout is created, meaning I get a two-second countdown, then 3 second etc.
What would be the correct way to handle this?
My component:
class Question extends Component {
constructor(props){
super(props);
this.state = {
answer: "",
correct: false,
timer: 8
}
this.countDownTrack = null;
this.checkAnswer = this.checkAnswer.bind(this);
}
countDown() {
let number = this.state.timer;
if (number > 0 && this.state.correct === false){
number--
this.setState({
timer: number
})
} else {
this.props.sendAnswer(this.state.answer, this.props.randIndex);
this.setState({
timer: 8
})
}
}
//what we want is for there to be a single setTimeout
CountDownHandle = () => {
this.countDownTrack = setTimeout(()=>{
this.countDown();
},1000)
}
checkAnswer = (e) => {
e.preventDefault();
this.props.sendAnswer(this.state.answer, this.props.randIndex);
this.setState({
answer: "",
timer: 8
})
}
handleChange = (event) => {
this.setState({
answer: event.target.value
})
}
render(){
this.CountDownHandle();
let countShow;
countShow = this.state.timer;
return (
<div className="Question">
<h1>What is</h1>
<h1><span id="table1">{parseInt(this.props.no1)}</span> X <span id="table2">{parseInt(this.props.no2)}</span>?</h1>
<form action="" onSubmit={this.checkAnswer}>
<input autoComplete="off" type="text" name="answer" onChange={this.handleChange} value={this.state.answer}/>
</form>
<p>{countShow}</p>
</div>
);
}
}
export default Question;

you need to clear timeout re-rendering the component using this function:
clearTimeout(this.trackingTimer);
It maybe a part of else code block in countDown().

Remember to clear the timeout when the component is unmount via ComponentWillUnmount.
That will make sure there is no unwanted timer running on the background.

2 issues with your code structure:
If you want to show a visible countdown (eg. 8,7,6,...)
use "setInterval"
rather than "setTimeout"
Place the timer event trigger function outside of the render function, like in the componentDidMount, a button click, input focus, or a response to a singular action.
Also, to control the timeout to be singular, check if an instance exists 1st.
if(!this.countDownTrack)
this.countDownTrack = setInterval(
()=>{
this.countDown();
if(this.state.timer < 1)
clearInterval(this.trackingTimer);
},1000)

Related

start Interval again after clearing it

How can I restart Interval after clearing it? I want a button to be clickable once every 11 seconds I made it disabled while timer is > 0 after it's equal to 0 button isn't disabled so it's clickable I wrote this code it seems to work but after multiple call of a setInterval() function timer goes down too fast any soltuions>
data:{
sTimer:11,
sDisabled:true,
asd:null
},
methods:{
testing(){
this.sTimer--;
if(this.sTimer == 0){
clearInterval(this.asd);
this.sTimer= 11;
this.sDisabled = false;
}else{
this.sDisabled = true;
}
},
specialAttack(){
setInterval(() => this.testing(), 1000)
}
},
created(){
this.asd = setInterval(() => this.testing(), 1000);
}
<button class="specialAttack" :disabled="sDisabled" #click="specialAttack(); testing()">Special Attack {{ sTimer }}</button>
There are several mistakes you made here, so first of all to achieve such a thing you should use setTimeout instead of setInterval because interval generally will be repeated after a certain given time, so this may one of the possible reason for being called twice when you click your button as well. Then you need to create a generic function for this cause and then call it in document creation instead of creating two different intervals and calling them separately.
So with respecting these rules, your final code should be something like this:
HTML
<button class="specialAttack" :disabled="sDisabled" #click="specialAttack();">Special Attack {{ sTimer }}</button>
Javascript
data: () => {
return {
sTimer: 11,
sDisabled: false,
asd: null
};
},
methods: {
specialAttack() {
clearTimeout(this.asd); // Or as you suggest in comments you can disbale it here with replacing this: this.sDisabled = true
if (this.sTimer > 0) {
this.asd = setTimeout(() => {
this.sDisabled = true;
this.sTimer -= 1;
this.specialAttack();
}, 1000);
} else {
clearTimeout(this.asd);
this.sTimer = 11;
this.sDisabled = false;
}
}
},
created() {
this.specialAttack();
}
Here is a working demo:

How to use useEffect() correctly?

I want to change style by scrolling.
This code isn't working correctly
.
When I rolling up and down too many times and too fast, then the browser is going to freeze, crash.
I think I used useEffect() wrong. How can I solve this issue.
const ArticleItem = ({title, content, active, chapter, program, id, scrollPos}) => {
const ref = useRef(null);
const client = useApolloClient();
useEffect(() => {
if(ref.current.offsetTop <= (scrollPos + 200)) {
client.writeData({data: {
curChapter: chapter.num,
curArticle: id,
curProgram: program.name
}});
}
});
if(active === false)
return ( // Inactive Article
<div className='section-item' ref={ref}>
<h2>{title.toUpperCase()}</h2>
<ReactMarkdown source={content} />
<br />
</div>
)
return ( // Active Article
<div className='section-item active' ref={ref}>
<h2>{title.toUpperCase()}</h2>
<ReactMarkdown source={content} />
<br />
</div>
)
}
As a Result, I faced this warning.
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
I think this is the reason of issue?!
Based on my comment above, you need to include the dependency array for useEffect also. In the current way it is running infinitely. Probably you want to include scrollPos into that thus it would only triggered once scrollPos is changing.
Try the following:
useEffect(() => {
if(ref.current.offsetTop <= (scrollPos + 200)) {
client.writeData({data: {
curChapter: chapter.num,
curArticle: id,
curProgram: program.name
}});
}
}, [scrollPos]);
I hope this helps!
Well, the problem is triggered all the time you can use the scroll event listener and make your changes when this event is triggered.
const [scrollItem, setScrollItem] = useState(null);
const handleScroll() {
if(scrollItem) {
// logic goes here
}
}
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []); // initialize event only once
return (
<div ref={setScrollItem}>
...
</div>
);
EDIT
Avoid that solution, #norbitrial is right

ReactJS: how to hide element based on parent's props?

I have a component that is showing message box. It's called from the parent element something like that:
<AlertMessage status={this.state.status} text={this.state.text} />
I want to change it to take one more parameter hide and it would be hiding itself in 5 seconds:
<AlertMessage status={this.state.status} text={this.state.text} hide={true} />
The problem this alert message is based on props, so its render function looks like:
render(){
const statusName = this.props.status || 'info';
const alertStyle = 'alert-box ' + alertClasses[statusName] + (this.props.text ? '' : ' d-none');
return (
<div className={alertStyle}>
<span>{this.props.text}</span>
</div>
);
}
So, I see 2 ways to implement hiding after 5 seconds:
A parent element does it just setting up the text to ""
The alert component hides itself.
I don't want to involve the parent component here but I don't know how I would implement it inside alert component. Add more state like "hidden" to the alert component but how I would handle it after?
Update. I think I found the solution. I use static function getDerivedStateFromProps and the component's visibility is based on its state hide, not on the text:
static getDerivedStateFromProps(nextProps, prevState) {
if (prevState.text != nextProps.message && nextProps.message){
return {
text: nextProps.message,
hide: false
};
} else {
return {};
}
}
render(){
const statusName = this.props.status || 'info';
const alertStyle = this.state.hide ? ' d-none' : 'alert-box ' + alertClasses[statusName];
if (!this.state.hide){
setTimeout(() => this.setState({hide: true}), 5000);
}
}
There is still bug: when I show several messages in a row the total timeout will start on the first, not on the last, but it's okay for now.
It will be difficult to handle it properly without involving the parent element. If you clear the text prop on parent, the AlertMessage component will still be mounted, but will be no visible due to no content inside. The same situation if you would drop the logic inside AlertMessage - you can't "unmount" the component directly from inside it.
The disadvantage is that AlertMessage remains mounted, you may have problems with applying animations to it. Also it may take the space in your app (depends on css), so users can accidently click on it or may cause problems with clicking elements placed under it.
What I would suggest - use your hide prop. Inside that function, where you set hide to false and the alert appears - use setTimeout, so the hide prop goes true after n seconds.
const someFn = () => {
this.setState({ hide: false }); // alert appears
setTimeout(() => {
this.setState({ hide: true });
}, 5000); // alert disappears (gets unmounted)
};
Then inside your render:
{!hide && <AlertMessage status={this.state.status} text={this.state.text} />}
constructor(props) {
super(props);
this.state = {
hide: false
};
componentDidMount() {
this.timer = setTimout(() => this.setState({ hide: true }), 5000)
}
componentWillUnmount() {
this.timer && clearTimeout(this.timer);
render(){
const statusName = this.props.status || 'info';
const alertStyle = 'alert-box ' + alertClasses[statusName] + (this.props.text ? '' : ' d-none');
return (
<div className={alertStyle}>
<span>{this.state.hide ? "" : this.props.text}</span>
</div>
);
}
Just get a state variable hide. setTimeout will turn it to false after 5 seconds. Don't forget to clear it in componentWillUnmount.

In Vue app, detecting that a key press or mouse click happens

In my Vue app, I have a place where I'm setting a timer. I'd like to interrupt the timer if the user presses any key or clicks anywhere in the browser window.
I do not want to stop whatever they clicked from happening. I just want to get a callback whenever it does.
I could just put a function call in every handler, but that is both tedious, sloppy, and not very maintainable.
This is what I'm trying to achieve:
let timerId;
const startTheTimer = () => {
timerId = setTimeout(() => {
somestuff();
timerId = undefined;
}, 10000);
}
// Called whenever any mouse click or keyboard event happens
const userDidSomething = () => {
if (timerId) {
clearTimeout(timerId);
timerId = undefined;
}
}
So, my question is, how do I get the function userDidSomething() to be called?
Thanks.
Your question doesn't seem to have much to do with Vue. You'd just need to attach an event listener to document. For example
document.addEventListener('click', userDidSomething)
document.addEventListener('keydown', userDidSomething)
Note: Any event that is caught and has stopPropagation() called will not reach the document.
If your timer is set within a component, you should probably do all this in your component's mounted lifecycle hook.
It would also be a good idea to clear the timeouts and remove the listeners in the beforeDestroy hook.
For example
export default {
data () {
return {
timerId: null
}
},
methods: {
startTimer () {
this.timerId = setTimeout(...)
},
clearTimer () {
clearTimeout(this.timerId)
}
},
mounted () {
this.startTimer() // assuming you want to start the timer on mount
document.addEventListener('click', this.clearTimer)
document.addEventListener('keydown', this.clearTimer)
},
beforeDestroy () {
this.clearTimer()
document.removeEventListener('click', this.clearTimer)
document.removeEventListener('keydown', this.clearTimer)
}
}

React - Button Pressed, keep calling function

I'm trying to implement a zoom function. onClick works fine, but I'd like to have it when I hold the zoom button down, it zooms continuously. How can I implement this with ReactJS?
Jquery: mousedown effect (while left click is held down)
I was using this as a template, but onMousedown doesn't get registered according to console.log
<div className="zoomControl" >
<button className="zoomIn" onMouseDown={this.zoomIn}>+</button>
<button className="zoomOut" onClick={this.zoomOut}>-</button>
</div>
zoomIn = () => {
console.log('test');
var self = this;
this.timeout = setInterval(function(){
// Do something continuously
this.renderer.zoomIn();
}, 100);
return false;
};
zoomMouseUp = () => {
clearInterval(this.timeout);
return false;
};
You need to use both mouseUp and mouseDown. Start a time on mouseDown and call the zoom function with the timeout repeatedly and clear the time on mouseUp.
Here a demo with zoomIn and zoomOut to compare and better understand the algorithm.
Hope this helps!!
class Zoom extends React.Component {
constructor(props) {
super(props)
this.state = {
zoom: 1,
}
this.t = undefined
this.start = 100
this.repeat = this.repeat.bind(this)
this.onMouseDown = this.onMouseDown.bind(this)
this.onMouseUp = this.onMouseUp.bind(this)
this.zoom = this.zoom.bind(this)
this.zoomOut = this.zoomOut.bind(this)
}
zoom(){
this.setState({zoom: this.state.zoom + 0.1})
}
repeat() {
this.zoom()
this.t = setTimeout(this.repeat, this.start)
this.start = this.start / 2
}
onMouseDown() {
this.repeat()
}
onMouseUp() {
clearTimeout(this.t)
this.start = 100
}
zoomOut(){
this.setState({
zoom: 1
})
}
render() {
return <div className="zoomControl" >
<div className="zoom" style={{transform: 'scale('+ this.state.zoom +')'}}></div>
<button className="zoomIn" onMouseUp={this.onMouseUp} onMouseDown={this.onMouseDown}>+</button>
<button className="zoomOut" onClick={this.zoomOut}>-</button>
</div>
}
}
ReactDOM.render(<Zoom/>, document.getElementById('app'))
body {
overflow: hidden
}
.zoom {
width: 20px;
height: 20px;
margin: 0 auto;
background: red;
}
<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>
If you have to do some kind of animation here, you're better off using requestAnimationFrame than setting intervals. I'd do it something like this.
class App extends React.Component {
state = {
value: 0,
mousedown: false
}
zoom = () => {
if (this.state.mousedown) {
this.setState({ value: this.state.value + 1},
() => { window.requestAnimationFrame(this.zoom) }
)
}
}
zoomIn = () => {
window.requestAnimationFrame(this.zoom);
}
toggleMouseDown = () => {
this.setState({
mousedown: !this.state.mousedown
});
this.zoomIn()
}
render() {
return(
<div>
<button
onMouseUp={this.toggleMouseDown}
onMouseDown={this.toggleMouseDown}>
Click me
</button>
{/* The rest of your component goes here */}
</div>
);
}
}
It's hard to get all of the context, but I'll try to give a relevant answer:
You don't have any property set to call zoomMouseUp when you release the button. I'd start with:
<button className="zoomIn" onMouseDown={this.zoomIn} onMouseUp={this.zoomMouseUp} onMouseOut={this.zoomMouseUp}>+</button>
You stated that it starts zooming, but doesn't stop. That makes me assume it's working, so that should probably fix it. I added the onMouseOut because if they press the button and move the mouse away without releasing it, it's going to continue.
There are a lot of ways to do this, but that's probably the most simple with what you have.
My issue was due to right click being the primary click or some thing along the lines. It works fine as is.

Categories

Resources