React ref.current is undefined in useLayoutEffect

1.8k Views Asked by At

I'm trying to implement a POC for a horizontally scrollable section on a project. I have 3 h3s next to each other above the horizontally scrollable section, the h3s should correspond to the individual sub-sections in the horizontal bit.

I am using react-intersection-observer to create refs to the sub-sections to be able to scroll to them when a user clicks on one of the h3s. The state is being set correctly, and in React Dev Tools I can see that the useInView hooks are being set correctly and also show the s as refs. However, when I try to access refOne.current anywhere, it comes back undefined. I'm explicitly using useLayoutEffect here to wait for all DOM nodes to be fully painted, but still refOne.current is undefined.

import React, { useState, useLayoutEffect } from 'react';
import { useInView } from 'react-intersection-observer';
import classNames from 'classnames';

import classes from './Projects.module.css';
const Projects = () => {
    const [activeState, setActiveState] = useState('one');

    let [refOne, inViewOne, entryOne] = useInView({
        threshold: 1,
    });

    let [refTwo, inViewTwo, entryTwo] = useInView({
        threshold: 1,
    });

    let [refThree, inViewThree, entryThree] = useInView({
        threshold: 1,
    });

    useLayoutEffect(() => {

        console.log(refOne.current) // comes back undefined

        checkAndScroll(activeState);
    }, [activeState]);

    const setActiveHeading = (id) => {
        setActiveState(id);
    };

    let oneClasses = classNames(classes.heading, {
        [classes.activeHeading]: activeState === 'one',
    });
    let twoClasses = classNames(classes.heading, {
        [classes.activeHeading]: activeState === 'two',
    });
    let threeClasses = classNames(classes.heading, {
        [classes.activeHeading]: activeState === 'three',
    });

    const checkAndScroll = (id) => {

        console.log(refOne.current) // comes back undefined

        if (id === 'one' && !inViewOne) {
            scrollToRef(refOne);
        } else if (id === 'two' && !inViewTwo) {
            scrollToRef(refTwo);
        } else if (id === 'three' && !inViewThree) {
            scrollToRef(refThree);
        }
    };
    const scrollToRef = (ref) => {

        console.log(refOne.current) // comes back undefined

        if (ref && ref.current) {
            ref.current.scrollIntoView();
        }
    };

    return (
        <>
            <div className={classes.headingsContainer}>
                <h3
                    className={oneClasses}
                    id={classes.headingOne}
                    onClick={() => setActiveHeading('one')}
                >
                    One
                </h3>
                <h3
                    className={twoClasses}
                    id={classes.headingTwo}
                    onClick={() => setActiveHeading('two')}
                >
                    Two
                </h3>
                <h3
                    className={threeClasses}
                    id={classes.headingThree}
                    onClick={() => setActiveHeading('three')}
                >
                    Three
                </h3>
            </div>
            <div className={classes.container}>
                <div ref={refOne} className={classes.one} />
                <div ref={refTwo} className={classes.two} />
                <div ref={refThree} className={classes.three} />
            </div>
        </>
    );
};

export default Projects;

I've been working on this for hours and I am at the end of my wits. Any help is much appreciated!

2

There are 2 best solutions below

2
On

I'm explicitly using useLayoutEffect here to wait for all DOM nodes to be fully painted.

Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint.

React docs says this about useLayoutEffect.

Have you tried useEffect instead of useLayoutEffect?

0
On

I finally figured it out, and it's hella stupid.

The react-intersection-observer hook provides a ref, an inView boolean, and an entry object. However, the ref is not actually a ref.

The ref is a callback function which does not contain a current property (hence the undefined). See the creator's comment here: https://github.com/thebuilder/react-intersection-observer/issues/285#issuecomment-572980628

What you should do instead, is use the entry object it returns, and access the target property. If you have assigned your refs correctly, entry.target will contain the html node you assigned the ref to.