-1

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.

ss1319
  • 1
  • 1
  • 1
    Don't define components inside other components. Ther's a new `MyList` component created on every render of `App`. So on every render the old `` get's unmounted, the DOM destroyed, and a new instance of the new `` created and mounted. On Every Render! – Thomas Jan 23 '23 at 07:14
  • @Thomas Thank you for the tip, I'm a newb to React. After moving the MyList component out of the App component and forwarding a Ref from App to MyList, I was able to get the expected behavior, thank you for pointing me in the right direction! Working Code Sandbox: https://codesandbox.io/s/j8ml44 – ss1319 Jan 23 '23 at 09:14
  • For caching functions its recommend to use `useCallback` instead of `useMemo`, see the [React Beta docs](https://beta.reactjs.org/reference/react/useCallback) – RubenSmn Jan 23 '23 at 09:19
  • @RubenSmn using `useCallback` here would mean that OPs not only creating the arrow function, but on top of it calling `_.debounce` on every render and generating a debounced function that's passed to `useCallback`. `useMemo` is the more economic alternative in this case. – Thomas Jan 23 '23 at 12:25

1 Answers1

0

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);
ss1319
  • 1
  • 1