How to add an event listener that sets state in the onScroll event of a React functional component?

305 Views Asked by At

I am having a difficult time trying to set state of a variable during the onScroll event of a custom React functional component. A minimal working CodeSandbox can be seen here: https://codesandbox.io/s/j8ml44

import React, { useEffect, useState, useMemo, useCallback } from "react";
import _ from "lodash";
import ReactDOM from "react-dom";

export function App() {
  const [lastScrollTime, setLastScrollTime] = useState(0);
  const [nodeFound, setNodeFound] = useState(false);

  const ref = useCallback((node) => {
    if (node !== null) {
      console.log("node found");
      handleNodeRender();
    }
  }, []);

  const handleEndScroll = useMemo(
    () =>
      _.debounce(() => {
        console.log("stop");
        setLastScrollTime(0);
      }, 5000),
    []
  );

  const handleNodeRender = useMemo(
    () =>
      _.debounce(() => {
        console.log("render");
        setNodeFound(true);
      }, 100),
    []
  );
  useEffect(() => {
    const handleScroll = () => {
      if (lastScrollTime === 0) {
        console.log("start");
        setLastScrollTime(Date.now());
      }
      console.log("Scrolling");
      handleEndScroll();
    };
    console.log(document.querySelector("#scroll-0"));
    console.log(document.querySelector("#scroll-1"));
    if (nodeFound) {
      console.log("added listener");
      document
        .querySelector("#scroll-0")
        .addEventListener("scroll", handleScroll);
      document
        .querySelector("#scroll-1")
        .addEventListener("scroll", handleScroll);
    }
    return () => {
      if (nodeFound) {
        console.log("removed listener");
        document
          .querySelector("#scroll-0")
          .addEventListener("scroll", handleScroll);
        document
          .querySelector("#scroll-1")
          .removeEventListener("scroll", handleScroll);
      }
    };
  }, [lastScrollTime, nodeFound]);

  useEffect(() => {
    if (lastScrollTime === 0) {
      console.log("5 seconds waited");
    }
  }, [lastScrollTime]);

  const MyList = () => {
    return (
      <li>
        <div>
          <hr />
          <p>A</p>
          <div
            ref={ref}
            id="scroll-0"
            style={{ overflow: "auto", maxHeight: "70px" }}
          >
            <li>111</li>
            <li>222</li>
            <li>333</li>
            <li>444</li>
            <li>555</li>
            <li>666</li>
          </div>
        </div>
      </li>
    );
  };

  return (
    <div>
      <ul>
        <MyList />
        <li>
          <div>
            <hr />
            <p>B</p>
            <div
              ref={ref}
              id="scroll-1"
              style={{ overflow: "auto", maxHeight: "70px" }}
            >
              <li>111</li>
              <li>222</li>
              <li>333</li>
              <li>444</li>
              <li>555</li>
              <li>666</li>
            </div>
          </div>
        </li>
      </ul>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

When I scroll list B, the console logs and scroll event fires as expected (registers scroll event, logs 'scrolling', and then 5 seconds after scrolling stops, logs 'stop'). However, if I scroll list A, the scrollbox is buggy, to trigger the onScroll's handleEvent I have to attempt to scroll a few times before it will work.

The only difference between list A and B is that list A is extracted into its own functional component. How can I get list A to trigger the onScroll handleEvent event properly? I've tried adding keys, moving the functional component into its own file, but can't get it to work.

1

There are 1 best solutions below

0
ss1319 On

Thank you @Thomas for the help. Working code:

import React, { useEffect, useState, useMemo, useRef } from "react";
import _ from "lodash";
import ReactDOM from "react-dom";

const MyList = React.forwardRef(({ scrolling, setScrolling }, ref) => {
  const handleEndScroll = useMemo(
    () =>
      _.debounce(() => {
        setScrolling(false);
      }, 5000),
    []
  );
  useEffect(() => {
    const handleScroll = () => {
      if (!scrolling) {
        console.log("scrolling");
        setScrolling(true);
      }
      handleEndScroll();
    };
    let refTemp;
    if (ref.current) {
      refTemp = ref.current;
      ref.current.addEventListener("scroll", handleScroll);
      document
        .querySelector("#scroll-1")
        .addEventListener("scroll", handleScroll);
    }
    return () => {
      if (refTemp !== null) {
        refTemp.removeEventListener("scroll", handleScroll);
        document
          .querySelector("#scroll-1")
          .removeEventListener("scroll", handleScroll);
      }
    };
  }, [scrolling, ref, setScrolling, handleEndScroll]);
  return (
    <li>
      <div>
        <hr />
        <p>A</p>
        <div
          id="scroll-0"
          ref={ref}
          style={{ overflow: "auto", maxHeight: "70px" }}
        >
          <li>111</li>
          <li>222</li>
          <li>333</li>
          <li>444</li>
          <li>555</li>
          <li>666</li>
        </div>
      </div>
    </li>
  );
});

export function App() {
  const ref = useRef();
  const [scrolling, setScrolling] = useState(false);

  useEffect(() => {
    if (!scrolling) {
      console.log("5 seconds waited");
    }
  }, [scrolling]);

  return (
    <div>
      <ul>
        <MyList ref={ref} scrolling={scrolling} setScrolling={setScrolling} />
        <li>
          <div>
            <hr />
            <p>B</p>
            <div id="scroll-1" style={{ overflow: "auto", maxHeight: "70px" }}>
              <li>111</li>
              <li>222</li>
              <li>333</li>
              <li>444</li>
              <li>555</li>
              <li>666</li>
            </div>
          </div>
        </li>
      </ul>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);