zestyrx
zestyrx

Reputation: 89

State initialization inside a functional component (without infinite looping)

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

Answers (4)

Oliver
Oliver

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

Gilad Tamam
Gilad Tamam

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

Ricardo Gonzalez
Ricardo Gonzalez

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

Beginner
Beginner

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

  1. Passing nothing, like useEffect(() => {}) - it will call every time.

  2. Passing an empty array useEffect(() => {}, []) - it will call one time.

  3. Passing array deps, whenever the array dependencies changes it will execute the code block inside the usEffect.

    useEffect(() => { // some logic }, [user])

Upvotes: 1

Related Questions