DEV Community

Myles Ngicu
Myles Ngicu

Posted on

React Ref Problem: Ref Pointing to Multiple DOM Elements with CSS Media Query Hiding

Overview
In React, a single ref object can only hold a reference to one DOM element at a time via its .current property. Assigning the same ref to multiple elements causes the ref to point to the last rendered element. This can cause issues in event handling logic when multiple elements are rendered but some are hidden using CSS media queries.

Problem Description
I had a bug where the same React ref (locationRef) was assigned to two components that render differently for desktop and mobile views. This resulted in clicks inside the desktop view not working while clicks in the mobile view worked:

useEffect(() => {
  function handleClickOutside(event) {
    if (
      locationRef.current &&
      !locationRef.current.contains(event.target)
    ) {
      setIsLocationOpen(false);
    }
  }

  document.addEventListener("mousedown", handleClickOutside);
  return () => {
    document.removeEventListener("mousedown", handleClickOutside);
  };
}, [locationRef]);

// JSX structure
<DesktopOnly>
  <LocationContainer ref={locationRef}>
    {/* Desktop location toggle and menu */}
  </LocationContainer>
</DesktopOnly>

<MobileOnly>
  <LocationContainer ref={locationRef}>
    {/* Mobile location toggle and menu */}
  </LocationContainer>
</MobileOnly>

Enter fullscreen mode Exit fullscreen mode

In this setup:

The same locationRef is used on both desktop and mobile .

The mobile container is hidden via CSS media queries, e.g., display: none when on desktop screen sizes.

What Happens Internally?
Both desktop and mobile are present in the DOM; only one is visually hidden.

React assigns locationRef.current to the last rendered , typically the mobile one.

Even though the mobile container is hidden by CSS, locationRef.current points to its hidden DOM element.

The click outside handler checks events against the hidden element, causing flawed logic:

Clicks inside the visible desktop container may appear outside the referenced element.

Resulting in setIsLocationOpen(false) triggering unexpectedly.

Example Situation
If locationRef.current points to the mobile container which is hidden (but still in DOM due to CSS):

if (locationRef.current && !locationRef.current.contains(event.target)) {
  // This may trigger even if the click is inside the visible desktop container,
  // because locationRef.current points to the hidden mobile container node.
  setIsLocationOpen(false);
}
Enter fullscreen mode Exit fullscreen mode

Recommended Solutions
1. Use Separate Refs for Desktop and Mobile

const desktopLocationRef = useRef(null);
const mobileLocationRef = useRef(null);

useEffect(() => {
  function handleClickOutside(event) {
    const activeRef = window.innerWidth >= 768 ? desktopLocationRef : mobileLocationRef;

    if (
      activeRef.current &&
      !activeRef.current.contains(event.target)
    ) {
      setIsLocationOpen(false);
    }
  }

  document.addEventListener("mousedown", handleClickOutside);
  return () => {
    document.removeEventListener("mousedown", handleClickOutside);
  };
}, []);

<DesktopOnly>
  <LocationContainer ref={desktopLocationRef}>{/* desktop content */}</LocationContainer>
</DesktopOnly>

<MobileOnly>
  <LocationContainer ref={mobileLocationRef}>{/* mobile content */}</LocationContainer>
</MobileOnly>

Enter fullscreen mode Exit fullscreen mode

2. Conditionally Render Only One DOM Tree
Instead of hiding via CSS, conditionally render only the applicable container per screen:

return (
  <>
    {isDesktop ? (
      <LocationContainer ref={locationRefDesktop}>{/* desktop */}</LocationContainer>
    ) : (
      <LocationContainer ref={locationRefMobile}>{/* mobile */}</LocationContainer>
    )}
  </>
);
Enter fullscreen mode Exit fullscreen mode

This prevents multiple refs assigned to DOM elements simultaneously.

Top comments (0)