addEventListener('scroll') to scrollable <div /> using useRef - React

10,461

Solution 1

An easier approach for this particular use case might be to use the onScroll prop and use the scrollTop property from the event target to figure out if you should hide the image or not.

Example

const { useState } = React;

const App = props => {
  const [isLogoActive, setLogoActive] = useState(true);

  const onScroll = e => {
    setLogoActive(e.target.scrollTop < 100);
  };

  return (
    <div onScroll={onScroll} style={{ height: 300, overflowY: "scroll" }}>
      <p style={{ marginBottom: 200 }}>top</p>
      <img
        style={{
          width: 100,
          height: 100,
          visibility: isLogoActive ? "visible" : "hidden"
        }}
        src="https://arcweb.co/wp-content/uploads/2016/10/react-logo-1000-transparent-768x768.png"
      />
      <p style={{ marginTop: 200 }}>bottom</p>
    </div>
  );
};

ReactDOM.render(<App />, 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>

Solution 2

Here is the correct way to bind the addEventListener on div using useRef()

import React, { useState, useEffect, useCallback, useRef } from 'react';

function ScrollingWrapper(props) {
  const [hasScrolledDiv, setScrolled] = useState(false);
  const scrollContainer = useRef(null);
  const onScroll = useCallback((event) => {
    if(event.target.scrollTop > 125){
       setScrolled(true);
    } else if(event.target.scrollTop < 125) {
       setScrolled(false);
    }
}, []);

useEffect(() => {
    scrollContainerWrapper.current.addEventListener('scroll', onScroll);
    return () => scrollContainerWrapper.current.removeEventListener('scroll', onScroll);
},[]);

return (
    <div ref={scrollContainerWrapper}>
        {props.children}
    </div>
  );
 }

 export default ScrollingWrapper;

Solution 3

Depending on your use case, it's usually also good to throttle scroll event listeners, so they don't run on every pixel change.

const App = props => {
  const [isLogoActive, setLogoActive] = useState(true);

  const onScroll = useMemo(() => {
    const throttled = throttle(e => setLogoActive(e.target.scrollTop < 100), 300);
    return e => {
      e.persist();
      return throttled(e);
    };
  }, []);

  return (
    <div onScroll={onScroll}>
      <img
        style={{ visibility: isLogoActive ? 'visible' : 'hidden' }}
        src="https://arcweb.co/wp-content/uploads/2016/10/react-logo-1000-transparent-768x768.png"
      />
    </div>
  );
};

The throttle function is available in lodash.

Share:
10,461
Sam Kelham
Author by

Sam Kelham

Updated on June 15, 2022

Comments

  • Sam Kelham
    Sam Kelham almost 2 years

    This is one of the first times I am actually using React Hooks properly in a project so bear with me if I am not quite there.

    In the component below, my aim is to display the <HelperTooltip> on load and when the scrolling div (not the window) scrolls I want to hide after it scrolls X amount of pixels.

    My thought process is to create a useRef object on the scrolling <div/> element, which then I can add an event listens with a callback function which then can toggle the state to hide the <HelperTooltip>

    I have created a Codesandbox below to try and demonstrate what I am trying to do. As you can see in the demo the node.addEventListener('click') is working fine, however when I try and call the node.addEventListener('scroll') it is not firing.

    I'm not sure if I taking the wrong approach or not, any help will greatly be appreciated. In the codesandbox demo it is the react image that I trying to hide on scroll, not the <HelperTooltip>

    CodeSandbox link: https://codesandbox.io/s/zxj322ln24

    import React, { useRef, useCallback, useState } from "react";
    import ReactDOM from "react-dom";
    
    import "./styles.css";
    
    const App = props => {
      const [isLogoActive, toggleLogo] = useState(true);
    
      const scrollElementRef = useCallback(node => {
        node.addEventListener("click", event => {
          console.log("clicked", event);
        });
    
        /* 
          I want to add the scroll event listener 
          here and the set the state isLogoActive to 
          false like the event listener above but the 'scroll' event
          is firing --- see below on line 21
        */
    
        // node.addEventListener("scroll", event => {
        //   console.log("scrolled", event);
        //   toggle log
        // });
      });
    
      return (
        <div className="scrolling-container">
          <div ref={scrollElementRef} className="scrolling-element">
            <p>top</p>
    
            {isLogoActive && (
              <div className="element-to-hide-after-scroll">
                <img
                  style={{ width: "100px", height: "100px" }}
                  src="https://arcweb.co/wp-content/uploads/2016/10/react-logo-1000-transparent-768x768.png"
                />
              </div>
            )}
    
            <p>bottom</p>
          </div>
        </div>
      );
    };
    
    ReactDOM.render(<App />, document.getElementById("app"));
    
  • Sam Kelham
    Sam Kelham about 5 years
    Yep great spot about putting the ref on the wrong element! and thank you for giving me a comprehensive answer, works a treat. I will have to do some reading on the useEffect hook and see what it is all about. Thank you