Throttle function (lodash inspired) to handle resize event using React hooks only (with Sandbox)

All we need is an easy explanation of the problem, so here it is.

Sorry for the long question, but I needed to make an introduction in order to make it clearer.

I was in need of some code to toggle between my Headers components <HeaderDesktop> and <MobileHeader>.

At first I was using CSS media queries to toggle between them using display: block | none;. This is less then ideal, since both components will be rendered at the same time, which is inneficient and could bring problems in advertisement displays on hidden elements.

Someone here on SO suggested that I could use window.innerWidth and use React to determine which component to render based on that. That is much better indeed. Now only 1 component gets rendered at a time. This is what I did:

// INSIDE HEADER COMPONENT
return(
  <HeaderWrapper>
   {window.innerWidth < 1200 ?
      <HeaderMobile/>
    : <HeaderDesktop/>
   }
  </HeaderWrapper>
);

But I needed a way to handle resize events. So I did:

// INSIDE HEADER COMPONENT
const [windowSize, setWindowSize] = useState(window.innerWidth);

function handleResize() {
  setWindowSize(window.innerWidth);
}

return(
  <HeaderWrapper>
   {windowSize < 1200 ?
      <HeaderMobile/>
    : <HeaderDesktop/>
   }
  </HeaderWrapper>
);

Nice! That works, but now my component renders 1 trillion times a second every time a resize is happening. That’s no good for performance.

So I’ve done my research and found out about lodash throttle and debounce methods. Both can reduce and control the number of events handled, even when hundreds are fired subsequentely.

https://css-tricks.com/debouncing-throttling-explained-examples/

But I’m not a fan of bringing entire libraries to my dependency list just to use a simple functionality like that, so I’ve ended up creating the following effect hook to mimic the throttle functionality on my resize event handler.

// INSIDE HEADER COMPONENT

// Ref to store if there's a resize in progress
const resizeInProgress = useRef(false);

// State to store window size
const [windowSize, setWindowSize] = useState(window.innerWidth);

useEffect(() => {

  // This function trigger the resize event handler
  // And updates the ref saying that there's a resize in progress
  // If theres a resize in progress, it doesn't do anything

  function handleResize() {
    if (resizeInProgress.current === true) {
      return;
    }
    resizeInProgress.current = true;
    throttled_updateWindowSize();
  }

  // This function sets a timeout to update the state with the
  // new window size and when it executes, it resets the
  // resizeInProgress ref to false. You can execute what's the interval
  // You want to handle your resize events, in this case is 1000ms

  function throttled_updateWindowSize() {
    setTimeout(() => {
      console.log("I will be updated!");
      console.log(window.innerWidth);
      setWindowSize(window.innerWidth);
      resizeInProgress.current = false;
    }, 1000);
  }


  window.addEventListener("resize", handleResize);
  return () => window.removeEventListener("resize", handleResize);
}, []);

You can see this in action in the following Sandbox:

https://codesandbox.io/s/v3o0nmvvl0

enter image description here

QUESTION 1

Can you give me any suggestions on how to improve my code for the throttled version of the resize event handler?

QUESTION 2

I’m guessing I’ll be needing that functionality in other components. How can I make this easily reusable? Can I make this a custom Hook? I have never created one, so I’m still having some trouble on how to reason about them and what is the proper way to create them. Can you help me to put that into a Custom Hook?

Or would it better to create a Higher Order Component for that?

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 1

This isn’t something i’d do with a hook. You can get it to work as a hook, but you’re limiting yourself to only doing throttling inside components, when throttling is a more useful utility function than that, and hooks don’t make it any easier or let you do anything extra.

If you don’t want to import all of lodash, that’s understandable, but you could implement something similar yourself. Lodash’s throttle is a higher order function: you pass it in a function, and it returns you a new function that will only execute if the appropriate amount of time has passed since the last execution. The code to do that (without quite as many options and safety checks as lodash does) can be replicated like this:

const throttle = (func, delay) => {
  let inProgress = false;
  return (...args) => {
    if (inProgress) {
      return;
    }
    inProgress = true;
    setTimeout(() => {
      func(...args); // Consider moving this line before the set timeout if you want the very first one to be immediate
      inProgress = false;
    }, delay);
  }
}

To be used like this:

useEffect(() => {
  const handleResize = throttle(() => {
    setWindowSize(window.innerWidth);
  }, 1000);

  window.addEventListener("resize", handleResize);
  return () => window.removeEventListener("resize", handleResize);
}, []);

Note: Use and implement method 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply