Search code examples
reactjsreact-hookswindowevent-listenerscrolledwindow

How to properly add window scroll event listener in React for animating an element?


I have a React App and I am trying to use the useState and useEffect hooks to create something like an animation when the user scrolled more than 200px from the top of the page.
This is how my navigation looks like: enter image description here And I want to apply width: 0; style property to the image in the middle when the user scrolls down (when he scrolled more than 200px from top), otherwise, when the user scrolls back to the top to "bring it back" on the page with width: 150px;

This is my Navbar component:

import { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import logo from "../assets/images/logo.png";

import "../styles/navbar.scss";

const Navbar = () => {
  const [scrollY, setScrollY] = useState(0);

  const handleScroll = () => {
        const logo = document.querySelector("#logo");
        if (window.scrollY > 200) {
          logo.style.width = "0";
        } else {
          logo.style.width = "150px";
        }
        setScrollY(window.scrollY);
  };

  useEffect(() => {
    window.addEventListener("scroll", handleScroll);

    return window.removeEventListener("scroll", handleScroll);
  }, [scrollY]);

  return (
    <header>
      <nav>
        <Link to="/" className="nav-link">
          Grupuri
        </Link>
        <Link to="/" className="nav-link">
          Despre noi
        </Link>
        <Link to="/" className="nav-link">
          Live
        </Link>
      </nav>
      <img src={logo} alt="Maranata Lupeni" id="logo" />
      <nav>
        <Link to="/" className="nav-link">
          Rugaciune
        </Link>
        <Link to="/" className="nav-link">
          Credinta
        </Link>
        <Link to="/" className="nav-link">
          Contact
        </Link>
      </nav>
    </header>
  );
};

export default Navbar;

How can I solve this?


Solution

  • You have multiple issues that you need to address for your code to work:

    1. Return a cleanup function from useEffect (() => {). The function would be executed when the useEffect is called again (dependencies changed) or if the component unmounts. You returned the result of calling removeEventListener. Calling removeEventListener removed the event listener just after adding it.
    2. Don't change style using DOM methods directly, because React might re-render the element, and undo all changes. Use state with className or style properties.
    3. The useEffect should be executed only once. Define handleScroll inside the useEffect, and don't pass any redundant states (like scrollY).
    4. Use the correct state. Your component doesn't care about the current value of scrollY, just if you need to show or hide the element.

    Example:

    const { useState, useEffect } = React;
    
    const Demo = () => {
      const [hideLogo, setHideLogo] = useState(false);
    
      useEffect(() => {
        const handleScroll = () => {
          const logo = document.querySelector("#logo");
    
          setHideLogo(window.scrollY > 50);
        };
      
        window.addEventListener("scroll", handleScroll);
    
        return () => {
          window.removeEventListener("scroll", handleScroll);
        };
      }, []);
      
      return (
        <div 
          className="logo" 
          style={{ width: hideLogo ? 0 : '150px' }} />
      );
    }
    
    ReactDOM
      .createRoot(root)
      .render(<Demo />);
    .logo {
      height: 100px;
      margin: 0 auto;
      background: lightblue;
      transition: width 0.3s;
    }
    
    body {
      height: 300vh;
    }
    <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
    
    <div id="root"></div>

    Notes:

    1. The scroll event is called multiple times while scrolling. It's usually better to throttle/debounce it.
    2. If you need to know if an object is in/out the view use an Intersection Observer. It has better performance, and you can define the type and position of intersections, without the need to measure the positions of other elements.