Prevent re-render with expensive timer - reactjs

I need a timer in one of my main screens, which has to run every 10ms (which is really expensive, I know. It has to play a sound for a certain cadence).
I tried to use a custom hook to extract the time from the screen like so:
import { useEffect, useState } from "react";
// constants
const PLAYER_INTERVAL = 10;
const useStepSoundInterval = (
receivedCadence: number,
handleSound: () => void,
) => {
const [playerTimeCount, setPlayerTimeCount] = useState(0);
const [playerTimerStep, setPlayerTimerStep] = useState(0);
let timerSound: NodeJS.Timeout;
useEffect(() => {
timerSound = setInterval(handleStepSoundInterval, PLAYER_INTERVAL);
return () => {
clearInterval(timerSound);
setPlayerTimeCount(0);
setPlayerTimerStep(0);
};
}, []);
useEffect(() => {
if (receivedCadence) {
setPlayerTimerStep((playerTimerStep) => playerTimerStep + 0.1);
if (1 / receivedCadence * PLAYER_INTERVAL <= playerTimerStep) {
setPlayerTimerStep(0);
handleSound();
}
}
}, [receivedCadence, playerTimeCount]);
const handleStepSoundInterval = () => {
setPlayerTimeCount((playerTimeCount) => playerTimeCount + 1);
};
};
export default useStepSoundInterval;
Then I call it like that on my screen:
useStepSoundInterval(
receivedCadence,
handleSound,
);
I thought, if I extract it, as a hook, from the screen, it would prevent to re-render the JSX all the time, but it does re-render every 10ms, which is a tiny bit expensive :)
Can I somehow use React.Memo? I failed to implement it.

Related

How to stop useState hook setState re-render in the useEffect? I am trying to code a count down function and the mins can sync from another component

I am trying to code a countdown clock and the "mins" is from a brother component.
If I take off setMin from useEffect then it is not changing while the value change in source components, but if I leave it in useEffect it will re-rendering every time if the seconds change and makes it immutable. Anyway, can I fix this problem?
If I need useRef how can I use it with setMin?
export default function Timer(props) {
const { count } = props;
const [isPlay, setIsPlay] = useState(false);
const [isRestore, setIsRestore] = useState(false);
const [second, setSecond] = useState(0);
const [isBreak, setIsBreak] = useState(false);
const [min, setMin] = useState(count)
useEffect(() => {
setMin(count)
let alarm = document.getElementById("beep");
const countdown = () => {
setTimeout(() => {
if (second === 0 && min > 0) {
setMin(min-1);
setSecond(59);
} else if (min >= 0 && second > 0) {
setSecond(second - 1);
} else {
alarm.play();
alarm.addEventListener("ended", () => {
setIsBreak(!isBreak);
});
}
}, 1000);
};
if (isPlay) {
countdown();
} else {
alarm.pause();
}
if (isRestore) {
setIsPlay(false);
alarm.currentTime = 0;
setIsRestore(false);
}
},[isPlay, isRestore, second, isBreak,min,count]);
I think the majority of your logic should be outside of the useEffect hook. Take a look at this answer Countdown timer in React, it accomplishes pretty much the same task as yours and should help you get an idea of the necessary logic

Switching image src with images from an array on an interval in React

This should be fairly simple, but I keep getting a weird behaviour from the result.
Basically, I have an array of images:
const images = [img1, img2, img3, img4, img5, img6];
I also have an image index:
const [imageIndex, setImageIndex] = useState(0);
Then I do a little incrementation of the index:
const switchImage = () => {
if (imageIndex === images.length - 1) {
setImageIndex(0);
} else {
setImageIndex(imageIndex + 1);
}
return imageIndex;
}
Then I call this function from a useEffect:
useEffect(() => {
setInterval(() => {
switchImage();
}, 1000);
}, []);
And finally I add the html:
<img src={images[imageIndex]} />
The result is usually it gets stuck on the second image and stops incrementing, so I thought the issue might be with the useEffect and the way the component is rendering.
You need to use the second method signature of the useState setter function which gives you the previous state value to avoid the stale closure captured value.
const root = ReactDOM.createRoot(document.getElementById('root'));
const images = ['1','2','3','4','5','6'];
const Thing =()=>{
const [imageIndex, setImageIndex] = React.useState(0);
React.useEffect(() => {
setInterval(() => {
setImageIndex(prev => (
prev === images.length - 1 ? 0 : prev + 1
));
}, 1000);
},[])
console.log(imageIndex)
return (
<div>
<h1>{images[imageIndex]}</h1>
</div>
);
}
root.render(<Thing />);
See here https://codepen.io/drGreen/pen/JjpmQrV
Also worth seeing this link which is virtually identical.
In your case the useEffect which you have created it is only being triggered once; when the component is loading - that is because you did not define when this logic should be triggered by adding dependencies to the useEffect.
Now, since the component renders once, 'switchImage'()' is only being triggered once, hence, it iterates once, display the img and stops.
Here is some good documentation on useEffect if you would like to read more about it Using the Effect Hook - React
๐Ÿ’กHere is a slightly altered solution where we are using the debounce technique for the timer. SOLUTION๐Ÿ’ก
const root = ReactDOM.createRoot(document.getElementById('root'));
const images = ['๐Ÿ’ก','๐Ÿ˜Š','๐Ÿ˜','๐Ÿ˜','๐ŸŽฏ','๐Ÿ‘Œ'];
const DemoComponent = () =>{
const [imageIndex, setImageIndex] = React.useState(0);
//debounce set default 0.3s
const debounce = (func, timeout = 300) =>{
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}
// switch img fn.
const switchImage = () => {
setImageIndex(imageIndex === images.length - 1 ? 0 : imageIndex + 1)
return imageIndex;
}
//debounce switchImage and set timer to 1s
const switchImageDebounce = debounce(() => switchImage(),1000);
//useEffect
React.useEffect(() => {
switchImageDebounce()
}, [imageIndex]);
return (
<div>
<h1>{images[imageIndex]}</h1>
</div>
);
}
root.render();

React useRef() with setInterval() in useCallBack() function not clearing upon clearInterval()

I have this stopwatch code, which shows the elapsed time on screen. (React Component)
It all works okay, but calling "clearInterval(increment.current)" (which using using a useRef() for scope in React) doesn't seem to really "stop" the Interval. It keeps logging "Triggered" in the console (every second) - and I don't follow why it's still being called after clearInterval() when currentActivityOn === false. (And if the timer is triggered several times, it triggers more than once per second.)
Any suggestions?
import React, { useEffect, useRef, useCallback, useContext } from 'react'
import Container from '#material-ui/core/Container'
import Typography from '#material-ui/core/Typography'
import useLocalStorage from '../../hooks/useLocalStorage'
import { FlowTimeContext } from '../../services/flow/flow-time.context'
import { useStyles } from './grid-stopwatch.styles'
export default function GridStopwatch() {
const classes = useStyles()
const { currentActivityOn } = useContext(FlowTimeContext)
const [timerStartTime, setTimerStartTime] = useLocalStorage('timerStartTime', '')
const [timer, setTimer] = useLocalStorage('timer', 0)
const increment = useRef(null)
const handleTimerRun = useCallback(() => {
increment.current = setInterval(() => {
if(timerStartTime !== ''){
const new_timer = ((Math.floor(new Date().getTime() / 1000)) - timerStartTime)
setTimer(new_timer)
} else {
console.log("Triggered") // Why is this being called continually?
}
}, 1000)
}, [setTimer, timerStartTime])
const handleReset = useCallback(() => {
clearInterval(increment.current)
setTimer(0)
setTimerStartTime('')
}, [setTimer, setTimerStartTime])
const handleStart = useCallback(() => {
if(timerStartTime === ''){
var start = Math.floor(new Date().getTime() / 1000)
setTimerStartTime(start)
}
}, [timerStartTime, setTimerStartTime])
const formatTime = () => {
const getSeconds = `0${timer % 60}`.slice(-2)
const minutes = `${Math.floor(timer / 60)}`
const getMinutes = `0${minutes % 60}`.slice(-2)
const getHours = `0${Math.floor(timer / 3600)}`.slice(-2)
return `${getHours}h : ${getMinutes}m : ${getSeconds}s`
}
useEffect(() => {
if(currentActivityOn === true){
handleStart()
handleTimerRun()
} else {
handleReset()
}
}, [currentActivityOn, handleStart, handleReset, handleTimerRun])
return (
<Container maxWidth='xl' className={classes.container}>
{currentActivityOn && (
<Typography variant='subtitle2' gutterBottom>
Current Activity Duration: {formatTime()}
</Typography>
)}
</Container>
)
}
So the useEffect gets triggered twice in this case. So handleTimerRun got called twice, and it seems each time there was an additional interval started that was never cleared.
const handleTimerRun = useCallback(() => {
if(timerStartTime !== ''){
increment.current = setInterval(() => {
const new_timer = ((Math.floor(new Date().getTime() / 1000)) - timerStartTime)
setTimer(new_timer)
}, 1000)
}
}, [setTimer, timerStartTime, increment])
For now I'm able to work around this by moving the condition before starting the interval, so it only gets started once. This seems to solve my issue, but is a workaround rather than truly understanding "how" there can be an additional interval started that is assigned the same id. (and seemingly can't be "stopped")
Further clarity on that would certainly be appreciated.
It seems that if I console log increment.current, it keeps increasing - whereas I thought it would be a consistent ID...? (mutable yes, but consistent when not being mutated.)

How to use debounce with React

I've seen quite a few similar questions here, but none of the suggested solutions seemed to be working for me. Here's my problem (please notice that I'm very new to React, just learning it):
I have the following code in my App.js:
function App() {
const [movieSearchString, setMovieSearchString] = useState('')
const providerValue = {
moviesList: moviesList,
movieSearchString: movieSearchString,
}
const searchMovieHandler = () => {
console.log('movie handler called')
const params = {
apikey: 'somekey'
}
if (movieSearchString.length > 2) {
params.search = movieSearchString
}
debounce(() => {
console.log('deb: ' + movieSearchString)
}, 1000)
}
const movieInputChangeHandler = string => {
console.log('onMovieInputChange', string);
setMovieSearchString(string)
searchMovieHandler()
}
return (
<MoviesContext.Provider value={providerValue}>
<div className="App d-flex flex-column px-4 py-2">
<SearchBar
onMovieInputChange={movieInputChangeHandler}
/>
...Rest of the content
</div>
</MoviesContext.Provider>
);
}
In this situation all the console.logs get called EXCEPT the one that should be debounced (I tried both lodash debounce and my own, none worked; currently kept the lodash version).
So I tried to comment out that debounce call and tried to use it like that:
useEffect(() => {
console.log('use effect 1')
debounce(() => {
console.log('deb: ' + movieSearchString)
}, 1000)
}, [movieSearchString])
I'm getting the use effect 1 log when the movieSearchString changes, but not the debounced one.
So I tried to do this:
const debounced = useRef(debounce(() => {
console.log('deb: ' + movieSearchString)
}, 1000))
useEffect(() => debounced.current(movieSearchString), [movieSearchString])
In this case I'm getting the console log deb: after a second, but no movieSearchString is printed.
I don't know what else I can do here... Eventually what I want is when a user enters something in the text field, I want to send an API call with the entered string. I don't want to do it on every key stroke, of course, thus need the debounce. Any help, please?
Try to debounce the state value, not to debounce effect itself. It's easier to understand.
For example, you can use custom hook useDebounce in your project:
useDebounce.js
// borrowed from https://usehooks.com/useDebounce/
function useDebounce(value, delay) {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(
() => {
// Update debounced value after delay
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cancel the timeout if value changes (also on delay change or unmount)
// This is how we prevent debounced value from updating if value is changed ...
// .. within the delay period. Timeout gets cleared and restarted.
return () => {
clearTimeout(handler);
};
},
[value, delay] // Only re-call effect if value or delay changes
);
return debouncedValue;
}
App.js
const [movieSearchString, setMovieSearchString] = useState('')
const debouncedMovieSearchString = useDebounce(movieSearchString, 300);
const movieInputChangeHandler = string => {
setMovieSearchString(string)
}
useEffect(() => {
console.log('see useDebounceValue in action', debouncedMovieSearchString);
}, [debouncedMovieSearchString]);
useEffect(() => {
const params = {
apikey: 'somekey'
}
if (movieSearchString.length > 2) {
params.search = debouncedMovieSearchString
}
callApi(params);
}, [debouncedMovieSearchString]);
Refer to this article: https://usehooks.com/useDebounce/
You need to wrap the function you want to debounce wit the debounce() function like this:
const searchMovieHandler = debounce(() => {
console.log('movie handler called')
const params = {
apikey: 'somekey'
}
if (movieSearchString.length > 2) {
params.search = movieSearchString
}
}, 1000);

Adding seconds to Intl.dateTimeFormat with React useEffect, useState, and setInterval

I have come across a weird problem where changing the order of a clone inside a setState() hook function changes the expected behavior.
I am trying to add one second to the value each second. However doing this directly causes the seconds to increase by two instead of one.
This works
const [value, setValue] = useState(new Date());
useEffect(() => {
const interval = setInterval(
() =>
setValue((value) => {
const clonedDate = new Date(value.getTime());
clonedDate.setSeconds(clonedDate.getSeconds() + 1); // Add one second to the time
return clonedDate;
}),
1000
);
return () => {
clearInterval(interval);
};
}, []);
This adds two seconds instead of one
const [value, setValue] = useState(new Date());
useEffect(() => {
const interval = setInterval(
() =>
setValue((value) => {
value.setSeconds(value.getSeconds() + 1);
const clonedDate = new Date(value.getTime());
return clonedDate;
}),
1000
);
return () => {
clearInterval(interval);
};
}, []);
The only thing that is clear to me as that in the second version a state mutation is obviously occurring, but to be honest it isn't clear to me exactly where. It seems as though even though you are creating a new javascript Date object that it is still referencing properties of the previous data object.
Consider the following examples that exhibit identical behavior:
function App() {
const [value1, setValue1] = useState({ c: 0 });
const [value2, setValue2] = useState({ c: 0 });
useEffect(() => {
const interval = setInterval(
() =>
setValue1((value) => {
const clonedValue = { ...value }; // shallow copy first
clonedValue.c = clonedValue.c + 1; // update copy
return clonedValue; // return copy
}),
1000
);
return () => {
clearInterval(interval);
};
}, []);
useEffect(() => {
const interval = setInterval(
() =>
setValue2((value) => {
value.c = value.c + 1; // mutate current
const clonedValue = { ...value }; // shallow copy
return clonedValue; // return copy
}),
1000
);
return () => {
clearInterval(interval);
};
}, []);
return (
<div className="App">
<h1>Non-mutaiton Version</h1>
{value1.c}
<h1>Mutation Version</h1>
{value2.c}
</div>
);
}
Interestingly though, if you remove the React.StrictMode from around App the two perform identically.
StrictMode currently helps with:
Identifying components with unsafe lifecycles
Warning about legacy string ref API usage
Warning about deprecated findDOMNode usage
Detecting unexpected side effects
Detecting legacy context API
Detecting unexpected side effects
Strict mode canโ€™t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:
Class component constructor, render, and shouldComponentUpdate methods
Class component static getDerivedStateFromProps method
Function component bodies
State updater functions (the first argument to setState)
Functions passed to useState, useMemo, or useReducer
Demo with your original date objects running in both StrictMode and non-StrictMode:
The issue is with below line it is mutating the value object. You don't need to call value.setSeconds, value.getSeconds()+1 itSelf is enough to increment seconds by 1.
Replace below line of code -
const interval = setInterval(
() =>
setValue2((value2) => {
let second = value2.getSeconds() +1;
//value2.setSeconds(second);
let newTime = new Date();
newTime.setSeconds(second);
const clonedDate = new Date(newTime.getTime());
return clonedDate;
}),
1000
);

Resources