React: how to avoid re-rendering a component with a hook that returns always the same value but changes its inner state

Viewed 970

imagine to have this hook that changes his value every second and returns true if the random value is a multiple of 5 otherwise false. what can I do to stop re-rendering every second?

PS: I tried useMemo, and useRef to return back always the same object but it's still re-rendering.

please help

const useRandomIs5x = () => {
  const [state, setState] = useState(0);
  useEffect(() => {
    const t0 = setInterval(() => {
      setState(getRandomValue())
    }, 1000)
    return () => clearInterval(to)
  }, [])
  return state % 5 === 0;
}


const Root = () => {
  const bool = useRandomIs5x();
  console.log("I'm re-rendering every second", bool)
  return <div>test</div>
}

4 Answers

I believe this is barely doable without some work-arounds. To call setState or not, we have to access the current state value. This is possible if we pass state in the dependency array. But then, the interval will be re-created with every second.

It could be also doable with refs, however there's no a correct way (yet) to listen to ref changes.

Update: It looks like it works fine with usage of useRef as the previous data holder. Thanks to anhau2.

const { useEffect, useState, useRef } = React;

const useRandomIs5x = () => {
  const [state, setState] = useState(true);
  const ref = useRef(null);
  
  useEffect(() => {
    const t0 = setInterval(() => {
      const value = Math.floor(Math.random() * 5) % 5 === 0;
      if (value === ref.current) return;
      ref.current = value;
      setState(value);
    }, 1000);

    return () => clearInterval(t0);
  }, []);

  return state;
}

const Root = () => {
  const bool = useRandomIs5x();
  console.log('re-render!', bool);
  return <div>test</div>
}

ReactDOM.render(<Root />, document.getElementById("root"));
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="root"></div>

P.S. Looking forward for any other ideas how to deal with it.

You probably need to approach it a bit differently. You don't want the random value, you want whether it's divisible by 5. So that's what should be in state and returned from the hook. The random value can just be a ref. So try something like this (code not tested but should give you the general idea):

const useRandomIs5x = () => {
    const [divisibleBy5, setDivisibleBy5] = useState(true);
    const randomValue = useRef(0);

    useEffect(() => {
        const t0 = setInterval(() => {
            // Set the new random value into a ref so as to not cause re-render
            randomValue.current = getRandomValue();

            const randIsDivisibleBy5 = randomValue.current % 5 === 0;

            // Only if it changes do we update the boolean state and trigger re-render
            if (randIsDivisibleBy5 !== divisibleBy5) {
                setDivisibleBy5(randIsDivisibleBy5);
            }
        }, 1000);
        
        return () => clearInterval(to);
    }, []);

    // Return the boolean state value instead
    return divisibleBy5;
}

We better change the state to store is5x instead of the value. One possible solution is to use useRef to check whether we should update the value every second. And then, sync the useRef and the state value.

const useRandomIs5x = () => {
  const [state, setState] = useState(false);
  const prevIs5x = useRef(false);
  useEffect(() => {
    const t0 = setInterval(() => {
      const is5x = getRandomValue() % 5 === 0;
      const isDiff = is5x !== prevIs5x.current;
      prevIs5x.current = is5x;
      if (isDiff) {
        setState((prevState) => !prevState);
      }
    }, 1000);
    return () => clearInterval(t0);
  }, []);
  return state;
};
Related