'Get the ref of an element rendering by an array

I'm coding a tab navigation system with a sliding animation, the tabs are all visible, but only the selected tab is scrolled to. Problem is that, I need to get the ref of the current selected page, so I can set the overall height of the slide, because that page may be taller or shorter than other tabs.

import React, { MutableRefObject } from 'react';
import Props from './Props';
import styles from './Tabs.module.scss';

export default function Tabs(props: Props) {
    const [currTab, setCurrTab] = React.useState(0);
    const [tabsWidth, setTabsWidth] = React.useState(0);
    const [currentTabHeight, setCurrentTabHeight] = React.useState(0);
    const [currentTabElement, setCurrentTabElement] = React.useState<Element | null>(null);
    const thisRef = React.useRef<HTMLDivElement>(null);
    let currentTabRef = React.useRef<HTMLDivElement>(null);
    let refList: MutableRefObject<HTMLDivElement>[] = [];

    const calculateSizeData = () => {
        if (thisRef.current && tabsWidth !== thisRef.current.offsetWidth) {
            setTabsWidth(() => thisRef.current.clientWidth);
        }

        if (currentTabRef.current && currentTabHeight !== currentTabRef.current.offsetHeight) {
            setCurrentTabHeight(() => currentTabRef.current.offsetHeight);
        }
    }

    React.useEffect(() => {
        calculateSizeData();

        const resizeListener = new ResizeObserver(() => {
            calculateSizeData();
        });

        resizeListener.observe(thisRef.current);

        return () => {
            resizeListener.disconnect();
        }
    }, []);

    refList.length = 0;

    return (
        <div ref={thisRef} className={styles._}>
            <div className={styles.tabs}>
                { props.tabs.map((tab, index) => {
                    return (
                        <button onClick={() => {
                            setCurrTab(index);
                            calculateSizeData();
                        }} className={currTab === index ? styles.tabsButtonActive : ''} key={`nav-${index}`}>
                            { tab.label }

                            <svg>
                                <rect rx={2} width={'100%'} height={3} />
                            </svg>
                        </button>
                    )
                }) }
            </div>

            <div style={{
                height: currentTabHeight + 'px',
            }} className={styles.content}>
                <div style={{
                    right: `-${currTab * tabsWidth}px`,
                }} className={styles.contentStream}>
                    { [ ...props.tabs ].reverse().map((tab, index) => {
                        const ref = React.useRef<HTMLDivElement>(null);

                        refList.push(ref);

                        return (
                            <div ref={ref} style={{
                                width: tabsWidth + 'px',
                            }} key={`body-${index}`}>
                                { tab.body }
                            </div>
                        );
                    }) }
                </div>
            </div>
        </div>
    );   
}


Solution 1:[1]

This seems like a reasonable tab implementation for a beginner. It appears you're passing in content for the tabs via a prop named tabs and then keeping track of the active tab via useState() which is fair.

Without looking at the browser console, I believe that React doesn't like the way you are creating the array of refs. Reference semantics are pretty challenging, even for seasoned developers, so you shouldn't beat yourself up over this.

I found a good article that discusses how to keep track of refs to an array of elements, which I suggest you read.

Furthermore, I'll explain the differences between that article and your code. Your issues begin when you write let refList: MutableRefObject<HTMLDivElement>[] = []; According to the React hooks reference, ref objects created by React.useRef() are simply plain JavaScript objects that are persisted for the lifetime of the component. So what happens when we have an array of refs like you do here? Well actually, the contents of the array are irrelevant--it could be an array of strings for all we care. Because refList is not a ref object, it gets regenerated for every render.

What you want to do is write let refList = React.useRef([]), per the article, and then populate refList.current with refs to your child tabs as the article describes. Referring back to the React hooks reference, the object created by useRef() is a plain JavaScript object, and you can assign anything to current--not just DOM elements.

In summary, you want to create a ref of an array of refs, not an array of refs. Repeat that last sentence until it makes sense.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Vincent La