Sam Kelham
Sam Kelham

Reputation: 1467

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

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"));

Upvotes: 9

Views: 19159

Answers (4)

Harmeet Singh
Harmeet Singh

Reputation: 144

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(() => {
    scrollContainer.current.addEventListener('scroll', onScroll);
    return () => scrollContainer.current.removeEventListener('scroll', onScroll);
},[]);

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

 export default ScrollingWrapper;

Upvotes: 2

Nelu
Nelu

Reputation: 18770

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.

Upvotes: 2

Tholle
Tholle

Reputation: 112867

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>

Upvotes: 8

antoinechalifour
antoinechalifour

Reputation: 495

In your example, the scroll is not triggered on the scrolling-element but on the scrolling-container so that's where you want to put your ref : https://codesandbox.io/s/ko4vm93moo :)

But as Throlle said, you could also use the onScroll prop !

Upvotes: 0

Related Questions