1

I'm making a "sticky" header when scrollY meets offsetTop, but I don't want to hard code the offset. I've been attempting to store the initial offsetTop which can then be checked inside the isSticky() method attached to the scroll listener.

So far I've been unable to make the value stick (no pun intended) and it appears to be null inside the method being run.

const ProfileHeader: React.FC<ProfileHeaderData> = ({ profile }) => {
    const bannerUrl = getBannerImageUrl(profile.id, null, profile.bannerImageVersion);
    const logoUrl = getLogoImageUrl(profile.id, null, profile.logoImageVersion);
    const headerRef = useRef<HTMLDivElement>(null);

    const [sticky, setSticky] = useState("");
    const [headerOffSet, setHeaderOffSet] = useState<number>(null);

    console.log(`Outside: ${headerOffSet}`);

    // on render, set listener
    useEffect(() => {
        console.log(`Render: ${headerRef.current.offsetTop}`);
        setHeaderOffSet(headerRef.current.offsetTop);

        window.addEventListener("scroll", isSticky);
        return () => {
            window.removeEventListener("scroll", isSticky);
        };
    }, []);

    const isSticky = () => {
        /* Method that will fix header after a specific scrollable */
        const scrollTop = window.scrollY;
        let originalOffset = headerOffSet ?? headerRef.current.offsetTop;
        if (headerOffSet === null) {
            console.log(`Setting header off set`);
            setHeaderOffSet(originalOffset);
        }
        console.log(`top: ${scrollTop} | offset: ${originalOffset} | state: ${headerOffSet}`);
        const stickyClass = scrollTop >= originalOffset ? styles.sticky : "";
        setSticky(stickyClass);
    };

    return (
        <div className={styles.container}>
            <div className={styles.bannerContainer}>
                {profile.bannerImageVersion && (
                    <Image
                        src={bannerUrl}
                        layout='fill'
                        className={styles.banner}
                    />
                )}
            </div>
            <div ref={headerRef} className={`${styles.header} ${sticky}`}>
                <div className={styles.logoContainer}>
                    {profile.logoImageVersion && (
                        <Image
                            src={logoUrl}
                            layout='fill'
                            className={styles.logo}
                        />
                    )}
                </div>
                <FlagCircleIcon {...profile.country} size={32} />
                <h1 className={styles.displayName}>{profile.displayName}</h1>
            </div>
        </div>
    )
}

On the initial page load, I get the following console output:

enter image description here

As I start to scroll, the output is as follows:

enter image description here

Seems like the state is never set?

Jason Bert
  • 562
  • 1
  • 3
  • 13

1 Answers1

0

Found a blog post explaining that you need to use a reference (useRef) and access the value via that inside the listener.

https://medium.com/geographit/accessing-react-state-in-event-listeners-with-usestate-and-useref-hooks-8cceee73c559

This references another SO post:

React useState hook event handler using initial state

EDIT

Providing working example based on original question code and solution provided by the blog post. The change the key change is to have a references to the useState variables and read from those inside the event listener.

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

import styles from './testComponent.module.scss';

export const TestComponent: React.FC = ({ children }) => {
    const headerRef = useRef<HTMLDivElement>(null);

    const [sticky, _setSticky] = useState(false);
    const stickyRef = useRef(sticky);
    const setSticky = (data: boolean) => {
        stickyRef.current = data;
        _setSticky(data);
    }

    const [headerOffSet, _setHeaderOffSet] = useState<number>(null);
    const headerOffSetRef = useRef(headerOffSet);
    const setHeaderOffSet = (data: number) => {
        headerOffSetRef.current = data;
        _setHeaderOffSet(data);
    }

    const isSticky = () => {
        const scrollTop = window.scrollY;
        const isSticky = scrollTop >= headerOffSetRef.current;

        setSticky(isSticky);
    };

    // on render, set listener
    useEffect(() => {
        setHeaderOffSet(headerRef.current.offsetTop);

        window.addEventListener("scroll", isSticky);
        return () => {
            window.removeEventListener("scroll", isSticky);
        };
    }, []);

    return (
        <div className={styles.container}>
            <div ref={headerRef} className={`${styles.header} ${sticky ? styles.isSticky : null}`}>
                {children}
            </div>
        </div>
    )
}
Jason Bert
  • 562
  • 1
  • 3
  • 13