How to manually stop a watcher? - javascript

I need to stop watch() but the docs don't really explain how to do that.
This watcher runs until the loop is finished (1000 seconds):
const state = reactive({
val: 0
})
watch(() => state.val, () => {
console.log(state.val)
})
for (let i = 1; i < 1000; i++) {
setTimeout(function timer() {
state.val = state.val + 1
}, i * 1000);
}
How do I stop the watcher after running once? Using watchEffect is not an option because for my use case the watcher needs to run several times before stopped, which is not described in this simplified example. From my understanding watchEffect runs only once (after initiation).

The "watch" function returns a function which stops the watcher when it is called :
const unwatch = watch(someProperty, () => { });
unwatch(); // It will stop watching
To watch a change only once:
const unwatch = watch(someProperty, () => {
// Do what your have to do
unwatch();
});

Related

SetInterval causes too many re-renders React

Hello I would like to put setInterval in my React project to add 1 for each second but I got an error like in title of this post.
js:
const [activeTab, setActiveTab] = useState(0)
useEffect(() => {
setInterval(setActiveTab(prevTab => {
if (prevTab === 3) return 0
console.log('hi')
return prevTab += 1
}), 1000)
})
There are a few issues:
You're not passing a function to setInterval, you're calling setActiveTab and passing its return value into setInterval.
If you were passing in a function, you'd be adding a new repeated timer every time your componennt ran. See the documentation — useEffect:
By default, effects run after every completed render...
And setInterval:
The setInterval() method... repeatedly calls a function or executes a code snippet, with a fixed time delay between each call.
(my emphasis)
Starting a new repeating timer every time your component re-renders creates a lot of repeating timers.
Your component will leave the interval timer running if the component is unmounted. It should stop the interval timer.
To fix it, pass in a function, add a dependency array, and add a cleanup callback to stop the interval:
const [activeTab, setActiveTab] = useState(0);
useEffect(() => {
const handle = setInterval(() => { // *** A function
setActiveTab(prevTab => {
if (prevTab === 3) return 0;
console.log("?ghi");
return prevTab += 1;
});
}, 1000);
return () => { // *** Clear the interval on unmount
clearInterval(handle); // ***
}; // ***
}, []); // *** Empty dependency array = only run on mount
Side note: Assuming you don't need the console.log, that state setter callback function can be simpler by using the remainder operator:
setActiveTab(prevTab => (prevTab + 1) % 3);
useEffect(() => {
setInterval(() => {
setActiveTab((prevTab) => {
if (prevTab !== 3) {
return (prevTab += 1);
} else {
return 0;
} // console.log("hi");
});
}, 1000);
}, []);

Different intervals for setTimeout in useEffect

I am trying to achieve a situation when a function inside setTimeout is executed after X seconds first time, and then regularly at Y interval.
useEffect(() => {
let firstTime = true;
const timer = setTimeout(() => {
getValue(id).then((res) => {
setValue(res);
});
if (firstTime) {
firstTime = false;
}
}, firstTime ? 30000 : 60000);
return () => clearTimeout(timer);
}, [])
This doesn't work, however, as it sets firstTime to false before running the function. Is there a way to graciously achieve the result? Is it even possible with setTimeout?
You can use a variable as the setTimeout delay. In this case you would set firstTime to the delay you wanted to use the first time (30000), and then inside timer you would use if (firstTime == 30000) { firstTime = 60000; }. Using a different variable name here would make sense. Then set the setTimeout delay to just firstTime like }, firstTime);
Hopefully this is actually what you wanted.
useEffect(() => {
let firstTime = 30000;
const timer = setTimeout(() => {
getValue(id).then((res) => {
setValue(res);
});
if (firstTime == 30000) {
firstTime = 60000;
}
}, firstTime);
return () => clearTimeout(timer);
}, [])
Old (incorrect) answer:
You can use setTimeout with an initial delay to initialise a setInterval, which will repeat subsequent times at a different interval.

JavaScript On-click Function to start and stop Intervals

I have two onclick functions that performing start and stop functions using socket when i click start it start requesting images from server(i am using intervals) when i click stop the server is stopped but the request keep coming to the server. the intervals seems not stopped after clicking kindly review my code
Code:
var interval;
function onclick() {
interval = setInterval(() => {
socket.emit("get_image", "Click");
socket.on('send_image', (message) => {
setImage(message.image);
});
}, 350)
}
function onclicks() {
clearInterval(interval);
setImage(blanked);
}
I have tried to add clearInterval function but it seems not working
Variables declared at the top level of a component are redeclared on each render (here every time you call setImage()) so interval is undefined by the time onclicks() is called.
In order to maintain the value through renders you need to either use state (useState) or a ref (useRef) which won't lose it's value. In this case, since you don't want renders attached to the value of interval a ref is more appropriate. useRef() returns an object with a current property in which your value will be stored.
const interval = React.useRef(null);
function onclick() {
interval.current = setInterval(() => {
socket.emit("get_image", "Click");
socket.on('send_image', (message) => {
setImage(message.image);
});
}, 350);
}
function onclicks() {
clearInterval(interval.current);
interval.current = null; // reset the ref
setImage(blanked);
}
You probably also want to avoid setting multiple intervals. You may already be doing so, but if not you can check if interval is null or not before setting a new one.
const interval = React.useRef(null);
function onclick() {
if (interval.current === null) { // only set new interval if interval ref is null
interval.current = setInterval(() => {
socket.emit("get_image", "Click");
socket.on('send_image', (message) => {
setImage(message.image);
});
}, 350);
}
}
For me, the easiest way to solve this behavior was set the interval as a Ref:
import { useRef } from 'react';
// Initialize the interval ref
const interval = useRef();
// Create the interval
function onclick() {
interval.current = setInterval(() => {
socket.emit("get_image", "Click");
socket.on('send_image', (message) => {
setImage(message.image);
});
}, 350)
}
// Remove the interval
function onclicks() {
clearInterval(interval.current);
setImage(blanked);
}
Using this hook, the component will not re-render when you change the value remove/change the interval.

call a method in a specific times in every seconds

I want to call a method 5 times in every one second. for example I have a methodA which i want to execute this method in 5 times in every 1 second. so how can i do it.
methodA(){
console.log("called")
}
timePeriod(){
setimeout....
}
You can do it with setInterval()
const test = setInterval(() => console.log("Hello method"),1000);
setTimeout( () => clearInterval(test), 5000);
Hope it is what you need!
let timePeriod = 1000 // 1000 ms
function methodA(){
console.log("called")
}
setInterval(methodA, timePeriod)
notice here that we pass the function as an argument without () which in other words we're referencing the function. Note that if you pass the function name along with () it means you're calling it independently
Also, keep in mind that setInterval works a little bit different than you might think as it's a macro task asynchronous function in the event loop. if you don't know about the event loop simply all you need to know that setInterval doesn't actually call the function unless the rest of you script gets fired + the time period you specify
setInterval = the time needed to fire the rest of the script + the time period you specify
and what you can do to test this is:
const tick = Date.now(), timeLog = (val='---') => console.log(`${val} \n Elapsed: ${Date.now() - tick} ms`)
setInterval(_ => timeLog('smth from setInterval'), 1000)
for(let i=1;i<1000000000;i++){
// this loop will run for 1B times so that we get a clear time delay
}
timeLog('smth from normal console.log')
const Comp= () => {
const methodA=()=>{
// statement here
}
useEffect(()=>{
let a = setInterval(methodA,1000/5)
return(()=>{
clearInterval(a)
},[])
})
return(<></>)
}
Try to use interval inside useEffect hook.
One could control both, the interval itself and also the condition for terminating the running interval, by writing a utility/helper function which enables not only clocked functions but also clocked methods.
function clocked(interval, isTerminate, proceed, target) {
let count = 0;
let intervalId = setInterval(() => {
proceed.call(target ?? null);
if (isTerminate(++count)) {
clearInterval(intervalId);
}
}, interval);
}
const obj = {
text: "The quick brown fox jumps over the lazy dog.",
log() {
console.log(this.text);
},
};
function doTerminateWhen(counter) {
return counter >= 5;
}
setTimeout(() => { console.log('1.001 seconds passed'); }, 1001);
clocked(200, doTerminateWhen, obj.log, obj);
setTimeout(() => { console.log('1 second passed'); }, 1000);
.as-console-wrapper { min-height: 100%!important; top: 0; }
setInterval is being used her to call the methodA function at each once second and modCall function has a for loop to iterate and call the setInterval the number of times specified in its paramter
function methodA(i){
console.log('call '+i)
}
function modCall(len){
for(let i=0;i<len;i++){
setInterval(()=>methodA(i),1000)
}
}
modCall(5) //5 is number of time you want to call the methodA

Same logic but different behaviour in 'class' and in 'functional component'

Attempted to translate an example code from class to functional component and faced the problem.
the target file is in components/Wheel/index.js
Key function that causes problem
const selectItem = () => {
if (selectedItem === null) {
const selectedItem = Math.floor(Math.random() * items.length);
console.log(selectedItem);
setSelectedItem(selectedItem);
} else {
setSelectedItem(null);
let t= setTimeout(() => {
selectItem()
}, 500);
clearTimeout(t);
}
};
First time is normal,
from second time onward,
2 clicks are needed for the wheel to spin.
I had to add clearTimeout() or infinite loop is resulted, but the same does not happen in the original.
Original working example in class
My version in functional component.
MyVersion
Thank you.
What an excellent nuance of hooks you've discovered. When you call selectItem in the timeout, the value of selectedItem that is captured in lexical scope is the last value (not null).
There's two answers, a simple answer and a better working answer.
The simple answer is you can accomplish it be simply separating the functions: https://codesandbox.io/s/spinning-wheel-game-forked-cecpi
It looks like this:
const doSelect = () => {
setSelectedItem(Math.floor(Math.random() * items.length));
};
const selectItem = () => {
if (selectedItem === null) {
doSelect();
} else {
setSelectedItem(null);
setTimeout(doSelect, 500);
}
};
Now, read on if you dare.
The complicated answer fixes the solution for the problem if items.length may change in between the time a timer is set up and it is fired:
https://codesandbox.io/s/spinning-wheel-game-forked-wmeku
Rerendering (i.e. setting state) in a timeout causes complexity - if the component re-rendered in between the timeout, then your callback could've captured "stale" props/state. So there's a lot going on here. I'll try and describe it as best I can:
const [selectedItem, setSelectedItem] = useState(null);
// we're going to use a ref to store our timer
const timer = useRef();
const { items } = props;
// this is just the callback that performs a random select
// you can see it is dependent on items.length from props
const doSelect = useCallback(() => {
setSelectedItem(Math.floor(Math.random() * items.length));
}, [items.length]);
// this is the callback to setup a timeout that we do
// after the user has clicked a "second" time.
// it is dependent on doSelect
const doTimeout = useCallback(() => {
timer.current = setTimeout(() => {
doSelect();
timer.current = null;
}, 500);
}, [doSelect]);
// Here's the tricky thing: if items.length changes in between
// the time we rerender and our timer fires, then the timer callback will have
// captured a stale value for items.length.
// The way we fix this is by using this effect.
// If items.length changes and there is a timer in progress we need to:
// 1. clear it
// 2. run it again
//
// In a perfect world we'd be capturing the amount of time remaining in the
// timer and fire it exactly (which requires another ref)
// feel free to try and implement that!
useEffect(() => {
if (!timer.current) return;
clearTimeout(timer.current);
doTimeout();
// it's safe to ignore this warning because
// we know exactly what the dependencies are here
}, [items.length, doTimeout]);
const selectItem = () => {
if (selectedItem === null) {
doSelect();
} else {
setSelectedItem(null);
doTimeout();
}
};

Categories

Resources