# React: callback ref vs useEffect+ref for conditionally-rendered DOM **Date**: 2026-06-17 **Context**: home-studio F1 fork integration (TodayView rubber-band selection) **Severity**: high (silent failure — listeners never bind, no error) **Cost**: ~30 min debugging before I noticed ## The trap ```tsx function MyView() { const [loading, setLoading] = useState(true) const containerRef = useRef(null) useEffect(() => { const c = containerRef.current if (!c) return // ← runs ONCE on mount, c is null c.addEventListener('mousedown', handler) return () => c.removeEventListener(...) }, []) return (
{loading ? :
{...cards...}
}
) } ``` **First mount**: `loading=true`, the grid div doesn't render, `containerRef.current=null`, effect returns early, cleanup never gets registered. **Loading becomes false**: grid renders, ref attaches — but no effect re-runs (deps are `[]`), so the listener is never bound. **Symptom**: clicking the grid does nothing. No console error. No warning. ## The fix: callback ref ```tsx function MyView() { const [loading, setLoading] = useState(true) const containerRef = useRef(null) const setContainer = useCallback((node: HTMLDivElement | null) => { // Cleanup previous (StrictMode double-invoke) if (containerRef.current && (containerRef.current as any).__cleanup) { (containerRef.current as any).__cleanup() } containerRef.current = node if (!node) return node.addEventListener('mousedown', handler) ;(node as any).__cleanup = () => node.removeEventListener(...) }, []) return (
{loading ? :
{...cards...}
}
) } ``` The callback ref **fires every time React attaches or detaches the ref**, including after conditional rendering flips. And under React StrictMode's intentional double-invoke, the cleanup runs before the re-bind, so the listener doesn't double-up. ## When to use which | Scenario | Use | |---|---| | DOM is unconditionally mounted | `useRef` + `useEffect(() => { ref.current?.addEventListener(...) }, [])` is fine | | DOM is conditionally rendered (`{loading ? ... :
}`) | **Callback ref** (always) | | DOM may unmount and remount | **Callback ref** (always) | | React 18 StrictMode | **Callback ref** (StrictMode-safe) | ## Related rule Same family of bug: `useEffect` with empty deps on a piece of state that isn't ready at first mount will **silently miss** updates when the state eventually becomes ready. The defensive pattern is: if your effect reads `ref.current` or any "maybe-not-ready" value, gate the effect on a [loaded flag](https://react.dev/reference/react/useEffect#my-effect-runs-twice-when-the-component-mounts) OR use the callback-ref pattern that handles late-mounts naturally. ## Reference - v0.2.0 home-studio had StrictMode disabled because of this exact bug. - v0.3.0 (commit `64f677d`) re-enabled StrictMode after switching to callback ref. - React docs on refs: https://react.dev/reference/react/useRef#caveats