Throttle window scroll event in React with setTimeout

Viewed 2527

I want to throttle listening to my window scroll event every 100 milliseconds instead of on every scroll to improve performance. I attempted this using setTimeout, but my implementation just waits 100 milliseconds before listening to window scroll instead of throttling the scroll event.

I don't want to use lodash and would like to use as much vanilla JS as possible.

import { useRef, useEffect } from "react";
import "./styles.css";

const cardData = [
  {
    title: "Title One",
    description: "Description One"
  },
  {
    title: "Title Two",
    description: "Description Two"
  },
  {
    title: "Title Three",
    description: "Description Three"
  },
  {
    title: "Title Four",
    description: "Description Four"
  },
  {
    title: "Title Five",
    description: "Description Five"
  },
  {
    title: "Title Six",
    description: "Description Six"
  },
  {
    title: "Title Seven",
    description: "Description Seven"
  },
  {
    title: "Title Eight",
    description: "Description Eight"
  },
  {
    title: "Title Nine",
    description: "Description Nine"
  },
  {
    title: "Title Ten",
    description: "Description Ten"
  },
  {
    title: "Title Eleven",
    description: "Description Eleven"
  },
  {
    title: "Title Twelve",
    description: "Description Twelve"
  },
  {
    title: "Title Thirteen",
    description: "Description Thirteen"
  }
];

const Card = ({ card }) => (
  <div className="item">
    <h1>{card.title}</h1>
    <p>{card.description}</p>
  </div>
);

export default function App() {
  const slider = useRef(null);
  const itemWrap = useRef(null);
  const pan = useRef(null);
  const countRef = useRef(0);

  const handleOnRender = () => {
    const wh = window.innerHeight;
    pan.current.style.height = itemWrap.current.scrollWidth + wh / 1.25 + "px";
  };

  const handleScroll = () => {
    console.log("scrolling");
    if (window.scrollY <= pan.current.offsetTop) {
      slider.current.style.position = "relative";
    }
    if (window.scrollY >= slider.current.offsetTop) {
      slider.current.style.position = "fixed";
      countRef.current = window.scrollY - pan.current.offsetTop;
      itemWrap.current.style.transform = `translate3d(-${countRef.current}px, 0, 0)`;
    }
  };

  useEffect(() => {
    handleOnRender();
    const timer = setInterval(() => {
      window.addEventListener("scroll", handleScroll);
    }, 100);
    return () => {
      clearInterval(timer);
      window.removeEventListener("scroll", handleScroll);
    };
  }, []);

  return (
    <div className="App">
      <div className="box bg-tomato">Box - #1</div>
      <div className="box bg-thistle">Box - #2</div>
      <div ref={slider} className="slider">
        <h2>Box - #3 (Slider)</h2>
        <div ref={itemWrap} className="item-wrap">
          {cardData.map((card) => (
            <Card card={card} key={card.title} />
          ))}
        </div>
      </div>
      <div ref={pan} className="pan"></div>

      <div className="box bg-teal">Box - #4</div>
      <div className="box bg-violet">Box - #5</div>
    </div>
  );
}

How do I edit my setTimeout function to throttle the scroll event every 100 milliseconds instead of delaying 100 milliseconds?

Codesandbox link

3 Answers

You can do something like this, Create a throttle function and pass your function which you want to throttle:

function throttle (callbackFn, limit) {
    let wait = false;                  
    return function () {              
        if (!wait) {                  
            callbackFn.call();           
            wait = true;               
            setTimeout(function () {   
                wait = false;          
            }, limit);
        }
    }
}

window.addEventListener("scroll", throttle(handleScroll, 100));

You don't need to add an eventListener for scroll at all, because you're no longer listening for scroll events – just executing handleScroll every 100ms.

useEffect(() => {
  handleOnRender();
  const id = setInterval(handleScroll, 100);
  return () => clearInterval(id);
}, []);

Get rid of useEffect and just have the scroll event set to trigger handleScroll. Then use:

  let lastScroll = 0;
  const handleScroll = () => {
    if (Date.now() - lastScroll < 100) {
      return;
    }
    lastScroll = Date.now();
    if (window.scrollY <= pan.current.offsetTop) {
      slider.current.style.position = "relative";
    }
    if (window.scrollY >= slider.current.offsetTop) {
      slider.current.style.position = "fixed";
      countRef.current = window.scrollY - pan.current.offsetTop;
      itemWrap.current.style.transform = `translate3d(-${countRef.current}px, 0, 0)`;
    }
  };
Related