Function is executing even after leaving the page React js - reactjs

When I navigate from home i.e, "/" to "/realtime" useEffect hook start the video from webcam, then I added a function handleVideoPlay for video onPlay event as shown below.
<video
className="video"
ref={videoRef}
autoPlay
muted
onPlay={handleVideoPlay}
/>
For every interval of 100ms, the code inside setInterval( which is inside the handleVideoPlay function) will run, which detects facial emotions using faceapi and draw canvas.
Here is my handleVideoPlay function
const [ isOnPage, setIsOnPage] = useState(true);
const handleVideoPlay = () => {
setInterval(async () => {
if(isOnPage){
canvasRef.current.innerHTML = faceapi.createCanvasFromMedia(videoRef.current);
const displaySize = {
width: videoWidth,
height: videoHeight,
};
faceapi.matchDimensions(canvasRef.current, displaySize);
const detections = await faceapi.detectAllFaces(videoRef.current, new
faceapi.TinyFaceDetectorOptions()).withFaceLandmarks().withFaceExpressions();
const resizeDetections = faceapi.resizeResults(detections, displaySize);
canvasRef.current.getContext("2d").clearRect(0, 0, videoWidth, videoHeight);
faceapi.draw.drawDetections(canvasRef.current, resizeDetections);
faceapi.draw.drawFaceLandmarks(canvasRef.current, resizeDetections);
faceapi.draw.drawFaceExpressions(canvasRef.current, resizeDetections);
}else{
return;
}
}, 100);
};
The problem is when I go back the handleVideoFunction is still running, so for canvasRef it is getting null value, and it's throwing this error as shown below
Unhandled Rejection (TypeError): Cannot read property 'getContext' of null
I want to stop the setInterval block on leaving the page. I tried by putting a state isOnPage to true and
I set it to false in useEffect cleanup so that if isOnPage is true the code in setInterval runs else it returns. but that doesn't worked. The other code in useEffect cleanup function is running but the state is not changing.
Please help me with this, and I'm sorry if haven't asked the question correctly and I'll give you if you need more information about this to resolve.
Thank you

You need to clear your setInterval from running when the component is unmounted.
You can do this using the useEffect hook:
useEffect(() => {
const interval = setInterval(() => {
console.log('I will log every second until I am cleared');
}, 1000);
return () => clearInterval(interval);
}, []);
Passing an empty array to useEffect ensures the effect will only trigger once when it is mounted.
The return of the effect is called when the component is unmounted.
If you clear the interval here, you will no longer have the interval running once the component is unmounted. Not only that, but you are ensuring that you are not leaking memory (i.e. by indefinitely increasing the number of setIntervals that are running in the background).

Related

React state variable not update in callback function from IntersectionObserver

I'm trying to make an infinite scroll function with IntersectionObserver. When the callback is called I make a test to see if there is some loading happens and if so I want to cancel that new call. The function is that:
const loadMore = useCallback(() => {
if (loading) {
return
}
setLoading(true);
console.log('Loading');
setTimeout(() => {
console.log('Finished');
setLoading(false);
}, 5000);
}, []);
The problem is that when the watcher calls loadMore again, and there is a load going on at that moment, the loading state sometimes has the old value (false), and setTimeOut is called again even if there is another one running. If I put the loading in the dependency array, the function is called many times (in loop) because I change it's value inside it. I tried to use a useRef to control loading, it works in the function, but I also want to use the loading state in JSX to show or not a text, but with the useRef variable it doesn't work in JSX and I find it a little ugly having to use two variables to control almost the same thing. Is there a way to do this just with the state?
The observer code:
useEffect(() => {
const options = {
root: null,
rootMargin: '10px',
threshold: 1.0
}
const observer = new IntersectionObserver(loadMore, options);
if (loaderRef && loaderRef.current) {
observer.observe(loaderRef.current);
}
}, [loadMore]);

React state not updating inside setInterval

I'm trying to learn React with some simple projects and can't seem to get my head around the following code, so would appreciate an explanation.
This snippet from a simple countdown function works fine; however, when I console.log, the setTime appears to correctly update the value of 'seconds', but when I console.log(time) immediately after it gives me the original value of 3. Why is this?
Bonus question - when the function startCountdown is called there is a delay in the correct time values appearing in my JSX, which I assume is down to the variable 'seconds' being populated and the start of the setInterval function, so I don't get a smooth and accurate start to the countdown. Is there a way around this?
const [ time, setTime ] = useState(3);
const [ clockActive, setClockActive ] = useState(false);
function startCountdown() {
let seconds = time * 60;
setClockActive(true);
let interval = setInterval(() => {
setTime(seconds--);
console.log(seconds); // Returns 179
console.log(time); // Returns 3
if(seconds < 0 ) {
clearInterval(interval);
}
}, 1000)
};
Update:
The reason you are not seeing the correct value in your function is the way that setState happens(setTime). When you call setState, it batches the calls and performs them when it wants to in the background. So you cannot call setState then immediately expect to be able to use its value inside of the function.
You can Take the console.log out of the function and put it in the render method and you will see the correct value.
Or you can try useEffect like this.
//This means that anytime you use setTime and the component is updated, print the current value of time. Only do this when time changes.
useEffect(()=>{
console.log(time);
},[time]);
Every time you setState you are rerendering the component which causes a havoc on state. So every second inside of your setInterval, you are re-rendering the component and starting it all over again ontop of what you already having running. To fix this, you need to use useEffect and pass in the state variables that you are using. I did an example for you here:
https://codesandbox.io/s/jolly-keller-qfwmx?file=/src/clock.js
import React, { useState, useEffect } from "react";
const Clock = (props) => {
const [time, setTime] = useState(3);
const [clockActive, setClockActive] = useState(false);
useEffect(() => {
let seconds = 60;
setClockActive(true);
const interval = setInterval(() => {
setTime((time) => time - 1);
}, 1000);
if (time <= 0) {
setClockActive(false);
clearInterval(interval);
}
return () => {
setClockActive(false);
clearInterval(interval);
};
}, [time, clockActive]);
return (
<>
{`Clock is currently ${clockActive === true ? "Active" : "Not Active"}`}
<br />
{`Time is ${time}`}
</>
);
};
export default Clock;

react-native console.log inside componentWillUnmount

The root component in a very straightforward app ...
Any debugging code put in componentWillUnmount is not shown in debugger-window
const App = () => {
useEffect(
() => () => {
console.log('Will unmount - cleanup...'); // not logged
},
[]
);
};
Why's that?
And How can I debug that cleanup code...
The correct syntax is:
useEffect(() => {
// Logic to run on mounting and update
return () = {
// Clean up code
console.log('------'); // this will log every time when component unmounts
}
}, [dependency array]);
[] is the dependency array, if blank useEffect() will run on mounting only and its clean up function will execute every time. If the array have some value in it, then whenever that value change, useEffect() will again executes.
You can try to remove the first () =>.
Also, as an idea, your use effect will render just on the first render of the page, that's what it does when you send an empty array [] as argument.

React Hooks: Idiomatic way to ensure that useEffect runs only when the contents of an array argument to the custom hook change

I am creating a custom hook in React which sets up an event listener for a given set of events. A default set of events exists, and the consumer of this custom hook is not expected to customize these in the majority of use-cases. Generally, I want the event listeners to be added upon the mounting of a component and removed upon its un-mounting. However, adhering to the principles of hooks (and the eslint(react-hooks/exhaustive-deps) lint rule), I wish to gracefully handle changes to the list of events to watch. What is the most idiomatic way to achieve this with React hooks?
Assuming I would simply like to remove all event listeners and re-add them when the list of events changes, I could attempt the following:
const useEventWatcher = (
interval = 5000,
events = ['mousemove', 'keydown', 'wheel']
) => {
const timerIdRef = useRef();
useEffect(() => {
const resetInterval = () => {
if (timerIdRef.current) {
clearInterval(timerIdRef.current);
}
timerIdRef.current = setInterval(() => {
console.log(`${interval} milliseconds passed with no ${events.join(', ')} events!`);
}, interval)
};
events.forEach(event => window.addEventListener(event, resetInterval));
// Don't want to miss the first time the interval passes without
// the given events triggering (cannot run the effect after every render due to this!)
resetInterval();
return () => {
events.forEach(event => window.removeEventListener(event, resetInterval));
};
}, [events, interval]);
}
Unfortunately, this will not function as intended. Note that I would like to provide a default value for the events parameter. Doing that with the current approach means that events points to a different reference every time the custom hook runs, which means the effect runs every time as well (due to shallow dependency comparison). Ideally, I would like a way of having the effect depend on the contents of the array, rather than the reference. What is the best way of achieving this?
You can separate two side effects in two different useEffects.
You can run the initial resetInterval in the first useEffect on load.
You need to run it once, and you might use a dependency of [].
But then, you need to extract resetInterval outside the useEffect.
Another problem is that, now resetInterval is re-created during every render.
So you can wrap it in useCallback.
The first useEffect depends on resetInterval (which causes the useEffect run once on load, thus will call resetInterval)
Now you can subscribe all events in the second useEffect with dependencies on [events, interval, resetInterval] as suggested by "eslint(react-hooks/exhaustive-deps) lint rule".
The result would look like following &
You can check the working demo on CodeSandbox.
const useEventWatcher = (
interval = 2000,
events = ["mousemove", "keydown", "wheel"]
) => {
const timerIdRef = useRef();
const resetInterval = useCallback(() => {
if (timerIdRef.current) {
clearInterval(timerIdRef.current);
}
timerIdRef.current = setInterval(() => {
console.log(
`${interval} seconds passed with no ${events.join(", ")} events!`
);
}, interval);
}, [events, interval]);
useEffect(() => {
resetInterval();
}, [resetInterval]);
useEffect(() => {
events.forEach(event => window.addEventListener(event, resetInterval));
return () => {
events.forEach(event => window.removeEventListener(event, resetInterval));
};
}, [events, interval, resetInterval]);
};
Check out the working page (I set the interval to 2 seconds for demo purpose)
When you move the mouse around, scroll wheels or press keys, the console log won't appear. Once those events are not fired, then you will see the console log messages.
So we need to include events into deps but for sure we don't want endless render loop.
Option #1: use JSON.stringify or similar to pass string as dependency not an array
function useEventWatcher(events = ['click'])
useEffect(() => {
}, [JSON.stringifiy(events.sort())])
However ESLint will still complain so either suppress it or use de-stringify:
const eventsStringified = JSON.stringify(events.sort());
useEffect(() => {
const eventsUnstringified = JSON.parse(eventsStringified);
}, [eventStringified]);
Option #2: move setting default value into useMemo. So default values will be referentially the same while events parameter is not passed(so it is undefined)
function useEventWatcher(events) {
const eventsOrDefault = useMemo(() => eventsPassed || ['click'], [events]);
useEffect(() => {
}, [eventsOrDefault]);
}

Jest: setTimeout is being called too many times

I am testing a react component that uses setTimeout. The problem is that Jest is saying that setTimeout is called even though it clearly isn't. There is a setTimeout to remove something from the ui and another one to pause the timer when the mouse is hovering over the component.
I tried adding a console.log() where the setTimeout is and the console log is never called, which means the setTimeout in the app isn't being called.
//app
const App = (props) => {
const [show, setShow] = useState(true);
const date = useRef(Date.now());
const remaining = useRef(props.duration);
let timeout;
useEffect(() => {
console.log('Should not run');
if (props.duration) {
timeout = setTimeout(() => {
setShow(false)
}, props.duration);
}
}, [props.duration]);
const pause = () => {
remaining.current -= Date.now() - date.current;
clearTimeout(timeout);
}
const play = () => {
date.current = Date.now();
clearTimeout(timeout);
console.log('should not run');
timeout = setTimeout(() => {
setIn(false);
}, remaining.current);
}
return (
<div onMouseOver={pause} onMouseLeave={play}>
{ show &&
props.content
}
</div>
)
}
//test
it('Should not setTimeout when duration is false', () => {
render(<Toast content="" duration={false} />);
//setTimeout is called once but does not come from App
expect(setTimeout).toHaveBeenCalledTimes(0);
});
it('Should pause the timer when pauseOnHover is true', () => {
const { container } = render(<Toast content="" pauseOnHover={true} />);
fireEvent.mouseOver(container.firstChild);
expect(clearTimeout).toHaveBeenCalledTimes(1);
fireEvent.mouseLeave(container.firstChild);
//setTimeout is called 3 times but does not come from App
expect(setTimeout).toHaveBeenCalledTimes(1);
});
So in the first test, setTimeout shouldn't be called but I receive that its called once. In the second test, setTimeout should be called once but is called 3 times. The app works fine I just don't understand what is going on with jest suggesting that setTimeout is being called more than it is.
I'm experiencing the exact same issue with the first of my Jest test always calling setTimeout once (without my component triggering it). By logging the arguments of this "unknown" setTimeout call, I found out it is invoked with a _flushCallback function and a delay of 0.
Looking into the repository of react-test-renderer shows a _flushCallback function is defined here. The Scheduler where _flushCallback is part of clearly states that it uses setTimeout when it runs in a non-DOM environment (which is the case when doing Jest tests).
I don't know how to properly proceed on researching this, for now, it seems like tests for the amount of times setTimeout is called are unreliable.
Thanks to #thabemmz for researching the cause of this, I have a hacked-together solution:
function countSetTimeoutCalls() {
return setTimeout.mock.calls.filter(([fn, t]) => (
t !== 0 ||
!String(fn).includes('_flushCallback')
));
}
Usage:
// expect(setTimeout).toHaveBeenCalledTimes(2);
// becomes:
expect(countSetTimeoutCalls()).toHaveLength(2);
It should be pretty clear what the code is doing; it filters out all calls which look like they are from that react-test-renderer line (i.e. the function contains _flushCallback and the timeout is 0.
It's brittle to changes in react-test-renderer's behaviour (or even function naming), but does the trick for now at least.

Resources