Reputation: 89
I have a functional component that holds custom viewport values in its state, so it must use event listeners and measure the window size:
const AppWrap = () => {
// custom vw and vh vars
const [vw, setvw] = useState();
const [vh, setvh] = useState();
// gets the inner height/width to act as viewport dimensions (cross-platform benefits)
const setViewportVars = () => {
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// can be accessed in scss as vw(n), vh(n) OR in css as --vw * n, --vh * n
document.documentElement.style.setProperty('--vw', `${viewportWidth / 100}px`);
document.documentElement.style.setProperty('--vh', `${viewportHeight / 100}px`);
// can be accessed in child components as vw * n or vh * n
setvw(viewportWidth / 100);
setvh(viewportHeight / 100);
}
// I'd like to run this function *once* when the component is initialized
setViewportVars();
// add listeners
window.addEventListener('resize', setViewportVars);
window.addEventListener('orientationchange', setViewportVars);
window.addEventListener('fullscreenchange', setViewportVars);
return (
<App vw={vw} vh={vh}/>
);
}
The above code produces an error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
I can wrap setViewportVars()
in useEffect, but I don't see why this is necessary. My understanding of functional components is that they only run code outside of the return statement once, and that only the JSX would re-render on a state change.
Upvotes: 2
Views: 287
Reputation: 237
The answer to why you need to useEffect() to prevent the infinite re-render:
<AppWrap>
has state {vw}
and {vh}
. When <AppWrap>
is fired, setViewportVars()
immediately runs and updates that state. Because you updated the state, setViewportVars()
is then fired again (to keep in line with the react one way data flow which updates the state of {vw/vh}
and causes a re-firing of AppWrap
...which causes a re-firing of setViewportVars()
. At no point here have we allowed the DOM to get painted by the browser, we are just repeating the loop of:
init component > getHeight/Width > updateState > re-render component > getHeight/Width > ...
useEffect behaves differently than a regular render. useEffect fires only after a the DOM has been painted by the browser. Which means that the first cycle would finish (init component > browser paints DOM > useEffect(getHeight/Width) > |if state aka viewsize changed?| > re-render
)
For more info, check out Dan Abramov's blog on useEffect
Upvotes: 1
Reputation: 306
const AppWrap = () => {
// custom vw and vh vars
const [vw, setvw] = useState();
const [vh, setvh] = useState();
// gets the inner height/width to act as viewport dimensions (cross-platform benefits)
const setViewportVars = useCallback(() => {
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// can be accessed in scss as vw(n), vh(n) OR in css as --vw * n, --vh * n
document.documentElement.style.setProperty('--vw', `${viewportWidth / 100}px`);
document.documentElement.style.setProperty('--vh', `${viewportHeight / 100}px`);
// can be accessed in child components as vw * n or vh * n
setvw(viewportWidth / 100);
setvh(viewportHeight / 100);
}, []);
useEffect(() => {
window.addEventListener('resize', setViewportVars);
window.addEventListener('orientationchange', setViewportVars);
window.addEventListener('fullscreenchange', setViewportVars);
return () => {
window.removeEventListener('resize', setViewportVars);
window.removeEventListener('orientationchange', setViewportVars);
window.removeEventListener('fullscreenchange', setViewportVars);
}
}, []);
useEffect(() => {
// I'd like to run this function *once* when the component is initialized
setViewportVars();
}, []);
return (
<App vw={vw} vh={vh} />
);
}
Upvotes: 0
Reputation: 1879
You have to use useEffect
and pass empty array as dependencies, so this will only be excecuted once just like componentDidMount
:
useEffect(() => {
setViewportVars();
// add listeners
window.addEventListener('resize', setViewportVars);
window.addEventListener('orientationchange', setViewportVars);
window.addEventListener('fullscreenchange', setViewportVars);
}, []);
Upvotes: 3
Reputation: 9095
So in your case what happens is basically you call the function it will update the state, so again component will load again function will call so basically that goes to infinite loop
Solution
you can useEffect, so in useEffect if you pass the second argument which is an array as empty it will called only one time like the componentDidMount
useEffect(() => {
setViewportVars()
}, [])
So if you pass second argument
Passing nothing, like useEffect(() => {})
- it will call every time.
Passing an empty array useEffect(() => {}, [])
- it will call one time.
Passing array deps, whenever the array dependencies changes it will execute the code block inside the usEffect.
useEffect(() => {
// some logic
}, [user])
Upvotes: 1