Reassigning references to elements based on state

Viewed 27

I have an array that I am looping through to create div elements, and based on which of these divs is 'active' (derived from state) I want them to have the ref property. This ref is also the parameter for a hook I'm using that adds an onclick listener to that ref. This works ok when the first div is active, however when my state changes to make the 2nd div active, the onclick listener isn't active on the 2nd div. Any idea on how I can resolve this?

Component file:

const [active, setActive] = useState(0);

const ref = useRef<HTMLDivElement>(null);

const { element } = useClick(ref);

return (
        <div style={{ position: 'relative' }}>
            {cards.map((card, index) => (
                <div
                    {...(index === active ? { ref } : { 'aria-hidden': 'true' })}
                >
                    <Card>
                        <div className={styles.title}>{card}</div>
                    </Card>
                </div>
            ))}
        </div>
    );

Custom hook:

const useClick = (
    element: RefObject<HTMLElement>
): {
    element: RefObject<HTMLElement>;
} => {
    const onClick = (): void => {
        console.log('clicked');
    };

    useEffect(() => {
        if (!element.current) {
            return undefined;
        }
        element.current.addEventListener('click', onClick);

        return (): void => {
            element.current?.removeEventListener('click', onClick);
        };
    }, [element]);

    return { element };
};

export default useClick;

I'd expect the onclick handler to be removed from the first element when it is no longer active and for this to be moved to the 2nd div in the loop. I tried an approach with using an array of refs so that each div had a unique ref but wasn't able to achieve this when using the ref index in the parameter of my custom hook. I can see my state is changing (active) correctly but I've redacted this code as its quite long and complex with IP and doesn't seem to coincide with the problem

1 Answers

If all you care about is the onClick, use it directly without a ref and setting it manually in useEffect:

<div 
  {...(index === active 
    ? { onClick } 
    : { 'aria-hidden': 'true' })
  }
>

Why doesn't it work?

The ref that you're using is an object reference, which have the current property (ref = { current }). While current changes according to the div it's used on ref stays the same, and the useEffect is not triggered.

Change the dependencies to look at element.current, and also add onClick because it's a dependency of useEffect as well:

useEffect(() => {
    if (!element.current) {
        return undefined;
    }
    element.current.addEventListener('click', onClick);

    return (): void => {
        element.current?.removeEventListener('click', onClick);
    };
}, [element.current, onClick]);

Another option is to use a function ref with useState:

const [active, setActive] = useState(0);
const [ref, setRef] = useState(null); // the state is used as ref

const { element } = useClick(ref);

<div
  // use setRef as a function ref
  {...(index === active ? { ref: setRef } : { 'aria-hidden': 'true' })}
>

// in your custom hook
useEffect(() => {
    // in this case element doesn't have current
    if (!element) return undefined;
    
    element?.addEventListener('click', onClick);

    return (): void => {
      element?.removeEventListener('click', onClick);
    };
}, [element]);
Related