set interval not updating the variable - javascript

I am trying to capture the no of clicks by the user.
Which i wish to send to an api every 15 min,
I am useing setinterval inside useEffect to achive this, but the problem is even though the state is changing outside but not inside setinterval. setinterval is only giving the initial default value.
here is the code -
const Agent = (props: RouteComponentProps) => {
const [clicks, setClicks] = useState(0);
const handleOnIdle = () => {
console.log("user is idle");
console.log("last active", new Date(getLastActiveTime()));
console.log("total idle time: ", getTotalIdleTime() / 1000);
};
const handleOnActive = () => {
console.log("user is active");
console.log("time remaining", getRemainingTime());
};
const handleOnAction = () => {
console.log("user did something", clicks);
setClicks(clicks + 1);
};
const {
getRemainingTime,
getLastActiveTime,
getTotalIdleTime,
} = useIdleTimer({
timeout: 10000,
onIdle: handleOnIdle,
onActive: handleOnActive,
onAction: handleOnAction,
debounce: 500,
});
useEffect(() => {
let timer = setInterval(
() => alert(`user clicked ${clicks} times`),
1000 * 30
);
return () => {
clearInterval(timer);
setClicks(0);
};
}, []);
return (
<AgentLayout>
<div className="dashboard-wrapper py-3">
<Switch>
<Redirect
exact
from={`${props.match.url}/`}
to={`${props.match.url}/home`}
/>
<Route path={`${props.match.url}/home`} component={Home} />
<Route
path={`${props.match.url}/lead-details`}
component={leadDetails}
/>
<Route
path={`${props.match.url}/fill-details`}
component={FillDetails}
/>
<Route
path={`${props.match.url}/my-desktime`}
component={MyDesktime}
/>
<Redirect to="/error" />
</Switch>
</div>
</AgentLayout>
the alert is giving user clicked 0 times

The problem is that only the first version of clicks is used by the timer function, because it closes over the version as of the first time your component function is called. When you pass an empty dependency array to useEffect, the useEffect callback is only called once, the first time the component function is called (when mounting the component).
You can fix that by having the timer function use the setClicks method instead:
useEffect(() => {
let timer = setInterval(
() => {
setClicks(currentClicks => { // ***
alert(`user clicked ${currentClicks} times`); // ***
return currentClicks; // ***
}); // ***
},
1000 * 30
);
return () => {
clearInterval(timer);
// setClicks(0); // <=== Don't do this, the component is unmounting
};
}, []);
Now, the timer calls the setClicks using the callback form, which means it receives the current value of clicks. Because it returns that same value, it doesn't update the state of the component.
It's also possible to solve this by adding clicks as a dependency to the useEffect, but it's a bit complicated. The naive way would be just to do this:
// The naive way
useEffect(() => {
let timer = setInterval(
() => {
alert(`user clicked ${clicks} times`);
},
1000 * 30
);
return () => {
clearInterval(timer);
// setClicks(0); // <=== Don't do this, the user's click count would get reset every time
};
}, [clicks]);
// ^^^^^^−−−−−−−−−−−−− ***
That will mostly work, but it will restart the timer every time clicks changes, even if that means that instead of waiting 30 seconds, it waits 45 (because clicks changed after 15 seconds, which cancelled the previous interval and started it again). That probably wouldn't matter for a really short interval, but for a 30 second one it seems less than ideal. Doing it this way without messing up the timing of the interval requires that you remember when the next timer callback should have happened and adjusting the duration of the initial delay to match, which gets fairly complicated.
I didn't notice earlier, but any time you're updating state based on existing state, I recommend using the callback form. So your
const handleOnAction = () => {
setClicks(clicks => clicks + 1); // Note the function callback
};

Issues
You've a stale enclosure of the clicks state, closed over from the initial render cycle when the mounting effect ran and started the interval.
Your click updater should use a functional update to update the clicks count from the previous state. This covers if the user is somehow able to click faster then the react component lifecycle and enqueue more than one clicks update in a render cycle.
T.J.'s answer is good, but I consider the useState updater function to be a pure function and the alert(`user clicked ${currentClicks} times`); in the middle of it is a side-effect and (IMHO) should be avoided.
Solution
Update handleOnAction to use a functional state update.
const handleOnAction = () => {
console.log("user did something", clicks);
setClicks(clicks => clicks + 1);
};
I suggest using a React ref and additional useEffect to update it, to hold a cached copy of the clicks state for the interval callback to reference.
const clicksRef = useRef();
useEffect(() => {
clicksRef.current = clicks; // update clicks ref when clicks updates
}, [clicks]);
useEffect(() => {
const timer = setInterval(
() => alert(`user clicked ${clicksRef.current} times`), // use clicks ref instead
1000 * 30
);
return () => {
clearInterval(timer);
// TJ already covered removing this extra state update
};
}, []);

Related

React, can't access updated value of state variable inside function passed to setInterval() in useEffect()

I am building a simple clock app with React. Currently the countDown() function works, but I would like the user to be able to stop/start the clock by pressing a button. I have a state boolean called paused that is inverted when the user clicks a button. The trouble is that after the value of paused is inverted, the reference to paused inside the countDown() function passed to setInterval() seems to be accessing the default value of paused, instead of the updated value.
function Clock(){
const [sec, setSecs] = useState(sessionLength * 60);
const [paused, setPaused] = useState(false);
const playPause = () => {
setPaused(paused => !paused);
};
const countDown = () => {
if(!paused){
setSecs(sec => sec - 1)
}
}
useEffect(() => {
const interval = setInterval(() => {
countDown();
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
I'm assuming it has something to do with the asynchronous nature of calls to setState() in React, and/or the nature of scoping/context when using regular expressions. However I haven't been able to determine what is going on by reading documentation related to these concepts.
I can think of some workarounds that would allow my app to function as desired. However I want to understand what is wrong with my current approach. I would appreciate any light anyone can shed on this!
In your code, the useEffect is called only once when mounting the component.
The countdown function registered inside will have its initial value at the time when the useEffect/setInterval is called. So paused will only have the value when you initially mount the component. Because you are not calling countDown directly or updating its value inside your useEffect, it is not updated.
I think you could solve this issue like this:
interval.current = useRef(null);
const countDown = () => {
if(!paused){
setSecs(sec => sec - 1)
}
}
useEffect(() => {
clearInterval(interval.current);
interval.current = setInterval(countDown, 1000);
return () => {
clearInterval(interval.current);
};
}, [paused]);
your useEffect is dependent on the value of paused as it needs to create a new interval (with a different countdown function). This will trigger the useEffect not only on mount but every time paused changes. So one solution would be to clear the interval and start a new one with a different callback function.
Edit: You could actually improve it as you only want the interval to be running if the countDown function actually does something so this should work too:
useEffect(() => {
clearInterval(interval.current);
if(!paused) {
interval.current = setInterval(countDown, 1000);
}
return () => {
clearInterval(interval.current);
};
}, [paused]);

How to trigger a function at regular intervals of time using hooks and when certain criteria is met I want to clear the time interval?

I have a react component that performs a certain task at regular intervals of time after mounting. But I want to clear the interval once after a criterion is met. How can I achieve that?
My code
const [totalTime, setTotalTime] = React.useState(10000);
const foo = () => {
console.log("Here");
};
React.useEffect(() => {
const secondInterval = setInterval(() => {
if (totalTime > 0) setTotalTime(totalTime - 1000);
}, 1000);
return () => clearInterval(secondInterval);
});
React.useEffect(() => {
let originalInterval;
if (totalTime > 0)
originalInterval = setInterval(() => {
foo();
console.log(totalTime);
}, 5000);
return () => clearInterval(originalInterval);
}, []);
When I watch the console even after 10000ms It is still logging Here and also totalTime is always being 10000ms. I am not able to figure out what exactly is happening.
You may need to pass the older state as an argument to the setTotalTime updater function. You also may need to pass (another) state variable as a dependency to the useEffect hook so that the function is executed every time the state variable changes
React.useEffect(() => {
const secondInterval = setInterval(() => {
if (totalTime > 0) setTotalTime(totalTime => totalTime - 1000);
}, 1000);
return () => clearInterval(secondInterval);
}, [...]);
Depends on your criteria, and what you call a criteria, but you could just use another state and then useEffect on that another state:
const [criteriaIsMet,setCriteriaIsMet] = useState(false);
useEffect(() => { if(criteriaIsMet) {clearInterval(...)} },[criteriaIsMet])
And then somewhere when you do your actual "criteria logic" you just go ahead and do setCriteriaIsMet(true)
And how would you know Id of interval in above useEffect - again you could just create a special state that will store that id, and you could set it in your original useEffect
As for why your current code is not clearing the interval is because when you use useEffect with empty array as second argument, and return function in first function argument - that will be execute on component unmounting
And these are just one of numerous options you have actually :)

Why useEffect cleanup logging every time?

I'm implementing count down
and use useRef hook to using it when clean setTimeout when the user navigates to the next screen to avoid cancel all subscription warning and it's work!
But I have something weird when count - 1 i can see "hey" in the console! although not cleaning the setTimeOut!!
I don't want to clean it in this case but why should loggin every time count changes!
code snippet
const [seconds, setSeconds] = useState(40);
const countRef = useRef(seconds);
useEffect(() => {
if (seconds > 0) {
countRef.current = setTimeout(() => {
setSeconds(seconds - 1);
}, 1000);
} else {
setSeconds(0);
}
return () => {
console.log('hey'); // every count down it's appeared
clearTimeout(countRef.current);
};
}, [seconds]);
You see "hey" because you're using seconds as a dependency. So every time seconds changes, the effect must run again leading to the effect's destroy function (the function you returned from the effect) to be invoked.
Instead of having seconds as a dependency, you should instead have setSeconds.
const [seconds, setSeconds] = React.useState(10);
useEffect(() => {
let didUnsub = false;
const id = setInterval(() => {
setSeconds((prev) => {
// while the count is greater than 0, continue to countdown
if (prev > 0) {
return prev - 1;
}
// once count eq 0, stop counting down
clearInterval(id);
didUnsub = true;
return 0;
});
}, 1000);
return () => {
console.log("unmounting");
// if the count didn't unsubscribe by reaching 0, clear the interval
if (!didUnsub) {
console.log("unsubscribing");
clearInterval(id);
}
};
}, [setSeconds]);
If you look at the example below, you'll see that the effect is only run once, when the component is mounted. If you were to cause the component to dismount, the destroy function would be invoked. This is because the setState is a dispatch function and doesn't change between renders, therefor it doesn't cause the effect to continuously be called.
In the example you can click the button to toggle between mounting and dismounting the counter. When you dismount it notice that it logs in the console.
Example: https://codesandbox.io/s/gallant-silence-ui0pv?file=/src/Countdown.js

Debounce in React es6

I have a search input in one of my page and I'm trying to make sure that it does not make 10 requests in one second if the user types too fast.
Seems like debouncing is the way to go. I've read multiple blog posts and SO questions. I'm trying to use debounce from lodash. I must be doing something wrong since what happens is that all my call are passed, just later.
Here is my component code:
const Partners = (props: any) => {
const [query, setQuery] = useState("");
const { partners, totalPages, currentPage } = props.partnersList;
useEffect(() => {
props.getPartners();
}, []);
useEffect(() => debounce(() => props.getPartners(query), 10000), [query]);
const handleQueryChange = (e: any) => {
setQuery(e.target.value);
};
const handlePageChange = (e: React.ChangeEvent<unknown>, value: number) => {
props.getPartners(query, value);
};
return (
<React.Fragment>
<Sidebar />
<MainContent className="iamSearchPartners">
<h1>Partners</h1>
<TextField
className="iamSearchPartners__search_input"
variant="outlined"
size="small"
onChange={handleQueryChange}
value={query}
/>
<PartnersList partners={partners} />
{totalPages > 1 && (
<Pagination
currentPage={currentPage || 1}
totalPages={totalPages}
handlePageChange={handlePageChange}
/>
)}{" "}
</MainContent>
</React.Fragment>
);
};
As you can see, I have a useEffect listening to changes to query.
debounce creates a debounced version of the function that passed as the argument. In this specific case, calling it in useEffect will create a new debounced version of the function on every render.
To mitigate this, the debounced version could be created outside of the render function, so it is not recreated on every render.
const myDebouncedFunction = debounce((handler, query) => handler(query), 10000);
const Partners = (props: any) => {
// omitted ...
useEffect(() => {
myDebouncedFunction(props.getPartners, query);
}, [query]);
// omitted ...
useMemo or putting it into a useState could work as well.
Another thing: useEffect only called the returned debounced version since it's an arrow function without braces, making it return the result of the evaluation. useEffect leverages the return of it's function to be some kind of "unsubscribe" handler, see React docs about "Effects with cleanup". So whenever query changed (after the second time), the effect was calling the function. The version above should be called on every query change.
Personally, I would try to handle the call of the debounced version of the function in the handleQueryChange instead of a useEffect, to make the "when should it happen" more clear.
I end up doing this manually honestly. Check out the differences between debouncing and throttling. I think you want to throttle the requests. If you debounce, nothing is going to happen until the timer ends.
If you want to wait 10 seconds as in your example, nothing should happen if the user types at least once every ten seconds, until 10 seconds after the last type. As opposed to throttling where a request would go out every 10 seconds.
This approach is kind of a hybrid because we are making sure the last one still goes out (as a debounce would), and throttling may not do, but we are still sending requests while the user is typing.
const timer = useRef<number>(0);
const lastEventSent = useRef(Date.now());
useEffect(() => {
if (Date.now() - lastEventSent.current < 500) {
// The event was fired too recently, but we still want
// to fire this event after the timeout if it is the last
// one (500ms in this case).
timer.current = setTimeout(() => {
lastEventSent.current = Date.now();
props.getPartners(query)
}, 500);
} else {
// An event hasn't been fired for 500ms, lets fire the event
props.getPartners(query)
lastEventSent.current = Date.now();
// And if the user was typing, there is probably a timer, lets clear that
clearTimeout(timer.current);
}
// Cleanup the timer if there was one and this effect is reloaded.
return () => clearTimeout(timer.current);
}, [query]);

React useEffect Warning: Maximum update depth exceeded. Is there a way around this?

As shown in the code below, I am using the useEffect hook to watch for changes to a percentage variable, then set a timer to increment that variable in 1 second. The callback will occur as soon as the page loads, kicking the process off.
The percentage variable is used to render a line chart that increments until a passed value is reached. (It basically shows a % pass rate, but on load the bars will move across the screen up to their %.)
Everything works fine, but the console flags the maximum update depth warning which points out it's trying to prevent an infinite loop when i'm using setState inside useEffect. I get the point here, but I know I'll never pass in a value above 100 and so there will never be an infinite loop. I also need the recursive functionality to have my charts appear to populate dynamically.
Even if I decided to ignore the warning, it's creating error in my testing if I mount the component.
Is there a way I can tell react that this won't be an infinite loop and get the error to go away? Or do I need a different approach?
const ReportProgressSummary = ({result, title}) => {
const [percent, setPercent] = useState(0);
let timer = null;
useEffect( () => {
let newPercent = percent + 1;
if (newPercent > result) {
clearTimeout(timer);
return;
}
timer = setTimeout(setPercent(newPercent), 1000);
}, [percent]);
return (
<ContainerStyled>
<ChartContainerStyled>
<ChartTitleStyled>{title}</ChartTitleStyled>
<CheckboxStyled type="checkbox" />
</ChartContainerStyled>
<ChartContainerStyled>
<LineContainerStyled>
<Line trailWidth="2" trailColor="red" strokeWidth="4" strokeColor="green" percent={percent}/>
</LineContainerStyled>
<h6>{percent}%</h6>
</ChartContainerStyled>
</ContainerStyled>
)
};
export default ReportProgressSummary;
Thanks for any help
In timer = setTimeout(setPercent(newPercent), 1000);
setPercent(newPercent) will be called immediately. setTimeout requires a function (instead of a function call):
timer = setTimeout(() => setPercent(newPercent), 1000);
Another thing to keep in mind is that timer will be defined on each render, thus clearTimeout won't have any effect. To avoid this, you can create a ref so that the value will be remembered. Like so:
const timer = React.useRef()
using/setting the value is done to the current property
timer.current = ...
Working example:
const ReportProgressSummary = ({result, title}) => {
const [percent, setPercent] = useState(0);
const timer = React.useRef();
useEffect( () => {
let newPercent = percent + 1;
if (newPercent > result) {
clearTimeout(timer.current);
return;
}
timer.current = setTimeout(() => setPercent(newPercent), 1000);
}, [percent]);
return (
<ContainerStyled>
<ChartContainerStyled>
<ChartTitleStyled>{title}</ChartTitleStyled>
<CheckboxStyled type="checkbox" />
</ChartContainerStyled>
<ChartContainerStyled>
<LineContainerStyled>
<Line trailWidth="2" trailColor="red" strokeWidth="4" strokeColor="green" percent={percent}/>
</LineContainerStyled>
<h6>{percent}%</h6>
</ChartContainerStyled>
</ContainerStyled>
)
};
export default ReportProgressSummary;

Categories