Navbar hide and show on Scroll using Custom React Hooks
Pratik sharma

Pratik sharma @biomathcode

About: Full stack developer

Location:
New delhi
Joined:
Jan 4, 2020

Navbar hide and show on Scroll using Custom React Hooks

Publish Date: Jul 4 '21
29 11

Making a Custom React Hook

code:

/**
 * useScroll React custom hook
 * Usage:
 *    const { scrollX, scrollY, scrollDirection } = useScroll();
 */

 import { useState, useEffect } from "react";

 export function useScroll() {
    // storing this to get the scroll direction
   const [lastScrollTop, setLastScrollTop] = useState(0);
    // the offset of the document.body
   const [bodyOffset, setBodyOffset] = useState(
     document.body.getBoundingClientRect()
   );
    // the vertical direction
   const [scrollY, setScrollY] = useState(bodyOffset.top);
    // the horizontal direction
   const [scrollX, setScrollX] = useState(bodyOffset.left);
    // scroll direction would be either up or down
   const [scrollDirection, setScrollDirection] = useState();

   const listener = e => {
     setBodyOffset(document.body.getBoundingClientRect());
     setScrollY(-bodyOffset.top);
     setScrollX(bodyOffset.left);
     setScrollDirection(lastScrollTop > -bodyOffset.top ? "down" : "up");
     setLastScrollTop(-bodyOffset.top);
   };

   useEffect(() => {
     window.addEventListener("scroll", listener);
     return () => {
       window.removeEventListener("scroll", listener);
     };
   });

   return {
     scrollY,
     scrollX,
     scrollDirection
   };
 }
Enter fullscreen mode Exit fullscreen mode

Styles

I am using css-in-javascript to set the visibility of the nav bar but you can use whatever you like.

const styles = {
    active: {
      visibility: "visible",
      transition: "all 0.5s"
    },
    hidden: {
      visibility: "hidden",
      transition: "all 0.5s",
      transform: "translateY(-100%)"
    }
  }
Enter fullscreen mode Exit fullscreen mode

Finale code would look like this

import React from 'react';

import {useScroll} from './../../hooks/useScroll'

export default function Navbar() {
  const { y, x, scrollDirection } = useScroll();  

  const styles = {
    active: {
      visibility: "visible",
      transition: "all 0.5s"
    },
    hidden: {
      visibility: "hidden",
      transition: "all 0.5s",
      transform: "translateY(-100%)"
    }
  }

    return (
        <nav className="Header" style={scrollDirection === "down" ? styles.active: styles.hidden}   >
        <Link to="/" className="Header__link">
            <img src={Logo} height="50px" width="auto" alt="logo"/>
            <div className="Header__link__title">
              Chronology
            </div>
        </Link>  
        <ul className="flex">
            <li>
              <Link to="/about" className="Header__link">About</Link>
            </li>
            <li>
              <Link to="/blog" className="Header__link">Blogs</Link>
            </li>
          </ul>
        </nav>       
    )
}
Enter fullscreen mode Exit fullscreen mode

I know this is very lazy writing but it's already midnight.

I will write in detail about it.

I hope it help someone.

Comments 11 total

  • Andrew Bone
    Andrew BoneJul 5, 2021

    Hi, there are a couple of things here that could lead to some Jank so I thought I'd give you some pointers, I hope that's alright 😊.

    You're listener function is outside of the useEffect it's used in. This means it is remade on every draw, this isn't a huge problem with onclick events and stuff but when it comes to using them in a useEffect it means the the useEffect runs every draw (as does its return functions).

    In this instance you've not got a dependencies array on your useEffect anyway meaning it is ran every draw. It would be better to have an empty array as the dependencies as that means it will only run on mount and then return on unmount.

    You're calling getBoundingClientRect on each scroll that's quite a heavy function to be calling so often. It's worth remembering that window already knows how far it's scrolled so we can just get it off that.

    A couple of extra points I'd add in are the scroll event listener is heavy as it is so I'd include a context with your hook that a dev can use to use the same listener else where in the app and on the same note you could make the function more generic by giving not just the current x and y but also the last x and y then you don't need to include the direction as it's a simple calculation the dev can include else where.


    I think with just those couple of changes you could change the hook from a great concept with a good execution to a great concept with a great execution. I've made a quick demo with my changes for you to have a look at 😊

    • Muhammad Hasnain
      Muhammad HasnainJul 5, 2021

      Ooh yes! Adding event listeners like that is a really really good example of a memory leak. I once read an article where soundcloud developers said that their app was getting slower after a lot of use. It was because they had the same issue. Attaching events without removing them.

      Also, the Sandbox you shared, there is an empty folder called useScrollListener. The import names are a little confusing too. I also want to know why you didn't use a useRef hook instead of using the className attribute?

      • Andrew Bone
        Andrew BoneJul 5, 2021

        I've fixed the names, I changed the name part way through making the demo so I guess it got confused and kept made a new version rather than renaming.

        Controlling the classes like this means you know for sure what classes you element has if you start adding and removing them with JS you have to keep track of all the classes and make sure you don't remove any by accident, or even leave any on the element when you don't mean to.

    • Pratik sharma
      Pratik sharmaJul 5, 2021

      I agree with all the things that you have told me. The dependency things is a mistake on my side. But Other things that you added a context and that getBoundingClientRect function is a heavy function, I was not aware of those.

      I get it that we can calculate the direction when we need and don't really have to do that in custom hook itself.

      Thanks for taking your time

      Highly Appreciate it !!

      I think you do great code reviews, Andrew !

      I will be changing the article accordingly.

    • pedrotomas50
      pedrotomas50Oct 7, 2021

      For those who are using styled components.

      dev-to-uploads.s3.amazonaws.com/up...

    • Dinu B
      Dinu BJan 12, 2024

      @link2twenty Great analysis and improvements!

      If I may add two cents - the approach with


      nav.nav-bar--hidden {
      transform: translateY(-100%);
      }

      creates some flickering if the navbar has a transparent background and a blurring of the backdrop. This is quite common for navbars IMO, so it's worth pointing out the alternative here, which doesn't interfere with the blurring:


      nav {
      /* ... */
      transition: top 150ms ease-in-out;
      }
      nav.nav-bar--hidden {
      top: -100%;
      }

  • Chetan Atrawalkar
    Chetan AtrawalkarJul 6, 2021

    Thank You so much bro ❤️ Nice post so helpful 🤗

  • Micah Katz
    Micah KatzJan 7, 2023

    This has some Server-Side-Rendering errors with document. I have updated it to handle those and TypeScript as well :)

    import { useSsr } from 'usehooks-ts'
    
        function useScroll() {
            const { isBrowser } = useSsr()
    
            // storing this to get the scroll direction
            const [lastScrollTop, setLastScrollTop] = useState(0);
            // the offset of the document.body
            const [bodyOffset, setBodyOffset] = useState(
                isBrowser ? document.body.getBoundingClientRect() : { top: 0, left: 0 }
            );
            // the vertical direction
            const [scrollY, setScrollY] = useState<number>(bodyOffset.top);
            // the horizontal direction
            const [scrollX, setScrollX] = useState<number>(bodyOffset.left);
            // scroll direction would be either up or down
            const [scrollDirection, setScrollDirection] = useState<'down' | 'up'>();
    
            const listener = (e: Event) => {
                isBrowser && setBodyOffset(document.body.getBoundingClientRect());
                setScrollY(-bodyOffset.top);
                setScrollX(bodyOffset.left);
                setScrollDirection(lastScrollTop > -bodyOffset.top ? "down" : "up");
                setLastScrollTop(-bodyOffset.top);
            };
    
            useEffect(() => {
                window.addEventListener("scroll", listener);
                return () => {
                    window.removeEventListener("scroll", listener);
                };
            });
    
            return {
                scrollY,
                scrollX,
                scrollDirection
            };
        }
    
    Enter fullscreen mode Exit fullscreen mode

    Then you can do something like this

        React.useEffect(() => {
            setIsVisible(scrollDirection === 'down')
        }, [scrollDirection])
    
    Enter fullscreen mode Exit fullscreen mode

    I have it working on my website at micahkatz.com

  • Ramon Arana
    Ramon AranaAug 17, 2023

    Great article. I'll leave the easiest solution imo:

    I've use @link2twenty 's hook (first comment) with the following implementation in the menu component:

        const [direction , setDirection] = useState('')
        const scroll = useScrollListener()
    
        useEffect(() => scroll.y > 150 && scroll.y - scroll.lastY > 0 ? setDirection('down') : setDirection('up'),
        [scroll.y, scroll.lastY]);
    
        const navbar: any = {
            active: {
                visibility: "visible",
                transition: "all 0.5s"
            },
            hidden: {
                visibility: "hidden",
                transition: "all 0.5s",
                transform: "translateY(-100%)"
            }
        }
    
    Enter fullscreen mode Exit fullscreen mode
     <nav style={direction === 'up' ? navbar.active : navbar.hidden}>{children}</nav>
    
    
    Enter fullscreen mode Exit fullscreen mode

    Works perfectly, but you'll propable get type error at style prop, but you can fix it by giving "any" type to the navbar variable or just replacing style with two classes ( or being proficient at TS).

    :)

    Also, always check first if you are going up, because otherwise when the page renders the menu won't be visible (because the default value will be navbar.hidden). Hope this helps!

  • ScriptKavi
    ScriptKaviAug 21, 2024

    Why create one when you can get all awesome hooks in a single library?

    Try scriptkavi/hooks. Copy paste style and easy to integrate with its own CLI

Add comment