This content originally appeared on DEV Community and was authored by reactuse.com
Discover 100+ powerful React Hooks possibilities! Visit www.reactuse.com for comprehensive documentation with MCP (Model Context Protocol) support, or install via
npm install @reactuses/core
to supercharge your React development efficiency with our extensive hook collection!
Prologue
Three years ago, when I first received the requirement to “add exposure tracking to this card component,” I didn’t think much of it. Just an IntersectionObserver, right? Should take ten minutes, tops.
Looking back now, I was like a cocky junior developer who thought knowing a few APIs made them invincible. Little did I know that the best solutions are often the ones you don’t even notice.
The Dream API
On that sunny afternoon, I sketched out what I thought would be the perfect API on the whiteboard:
<Exposure traceId="user_card_exposure" traceData={{ userId: 123 }}>
<UserCard />
</Exposure>
“Look how elegant this is!” I told my senior colleague, Dave.
Dave just smiled—that knowing smile seniors give when they see juniors about to learn an expensive lesson. Now I understand it was the kind of patience that comes from watching others walk the same path you once did.
First Face-Plant: The Naive Wrapper Approach
Young me chose the most “obvious” solution—if I need to track visibility, just wrap it in a div:
const ExposureWrapper = (props: ExposureWrapperProps) => {
const { traceId, traceData, ...rest } = props;
const [io] = useState(
() => typeof IntersectionObserver === 'function'
? new IntersectionObserver(handleVisibilityChange)
: null,
);
const dom = useRef<HTMLDivElement>(null);
useEffect(() => {
const target = dom.current;
if (target) {
io?.observe(target);
}
return () => {
if (target) {
io?.unobserve(target);
}
io?.disconnect();
};
}, [io]);
function handleVisibilityChange(changes: IntersectionObserverEntry[]) {
const change = changes[0];
if (change.intersectionRatio > 0) {
const container = change.target;
io?.unobserve(container);
io?.disconnect();
if (traceId) {
analytics.track(traceId, traceData);
}
}
}
return (
<div ref={dom} {...rest}>
{/* Children go here */}
</div>
);
};
I coded it up smoothly, tests passed, and I was pretty pleased with my “efficiency.”
Then reality hit me like a freight train.
“Dude, your component totally broke my layout!” Sarah from the design system team stormed over to my desk.
“My Flexbox alignment is all messed up!”
“The Grid layout is completely screwed!”
That’s when I realized that in the CSS world, every additional DOM layer is a potential butterfly effect. That “harmless” wrapper div was like jamming an extra gear into a Swiss watch.
The Revelation: findDOMNode to the Rescue
During a particularly painful debugging session, I was digging through React docs when I stumbled upon the findDOMNode
API.
“Wait, this is exactly what I need!”
It felt like being lost in a maze for hours and suddenly spotting the exit sign.
export class Impr extends Component<ExposureWrapperProps> {
public io: IntersectionObserver | null = null;
constructor(props) {
super(props);
this.io = typeof IntersectionObserver === 'function'
? new IntersectionObserver(this.handleVisibilityChange)
: null;
}
componentDidMount() {
// This single line changed everything
const target = findDOMNode(this) as HTMLElement;
if (target) {
this.io?.observe(target);
}
}
componentWillUnmount() {
const target = findDOMNode(this) as HTMLElement;
if (target) {
this.io?.unobserve(target);
}
this.io?.disconnect();
}
handleVisibilityChange = changes => {
const change = changes[0];
if (change.intersectionRatio > 0) {
const container = change.target;
this.io?.unobserve(container);
this.io?.disconnect();
if (this.props.traceId) {
analytics.track(this.props.traceId, this.props.traceData);
}
}
};
render() {
const { children } = this.props;
return <>{children}</>;
}
}
After the redeployment, silence. Beautiful, peaceful silence. No layout complaints, no styling conflicts. The component was like a ghost—invisible but doing its job perfectly.
“Now THAT’S what I call professional work!” Dave gave me a pat on the back.
At that moment, I thought I had mastered the art of frontend development.
The Plot Twist: React 19’s “Betrayal”
Fast forward to this year, and React 19’s release taught me what it means to have the rug pulled out from under you.
“findDOMNode
has been removed from React 19″
Reading that line felt like being told your favorite restaurant had burned down. Like a martial artist who’d spent years perfecting a technique, only to discover it was now forbidden.
The React team’s explanation was very “corporate”: We recommend using refs to access DOM nodes instead. findDOMNode
breaks component encapsulation.
In theory, refs are indeed more modern:
// Theory looks great
function MyComponent() {
const ref = useRef();
useEffect(() => {
if (ref.current) {
io?.observe(ref.current);
}
}, []);
return <div ref={ref}>Content</div>;
}
But reality had other plans. When dealing with custom components, refs become useless:
// This won't work - CustomComponent doesn't have forwardRef
<Exposure>
<CustomComponent />
</Exposure>
Even when we tried to force-inject refs using Children.only
:
function VisibilityChange(props: any) {
const { children } = props;
const defaultRef = useRef();
const ref = children.ref || defaultRef;
// Complex ref handling logic...
return Children.only({ ...children, ref });
}
This approach has fatal flaws: it requires ALL child components to support refs, but in reality, tons of third-party components and legacy code don’t support forwardRef
.
The 3 AM Eureka Moment: Anchor Positioning
During another sleepless night (why do the best coding insights always come at 3 AM?), I had a crazy idea: if I can’t directly access the child component’s DOM, why not place a “landmark” right next to it?
function FindDOMNodeReplacement({ children }) {
const [renderAnchor, setRenderAnchor] = useState(true);
const anchorRef = useRef();
const findDomNodeRef = useRef();
useEffect(() => {
if (anchorRef.current) {
// The magic moment: find target through sibling relationship
findDomNodeRef.current = anchorRef.current.nextElementSibling;
setRenderAnchor(false); // Mission accomplished, destroy the evidence
console.log('Found it!', findDomNodeRef.current);
}
}, []);
return (
<>
{renderAnchor && (
<span
ref={anchorRef}
style={{
position: 'absolute',
visibility: 'hidden',
pointerEvents: 'none',
width: 0,
height: 0,
overflow: 'hidden'
}}
/>
)}
{children}
</>
);
}
The brilliance of this approach impressed even me:
- Insert a completely invisible anchor element
- Use DOM tree sibling relationships to locate the real target
- Remove the anchor immediately after finding the target, leaving no trace
It’s like a perfect spy mission—infiltrate, locate, extract, leave no evidence.
Phoenix Rising: The Ultimate Solution
Integrating the anchor positioning technique into our exposure component, we arrived at the ultimate solution for the modern era:
function Exposure({ children, traceId, traceData }) {
const [renderAnchor, setRenderAnchor] = useState(true);
const [io] = useState(() =>
typeof IntersectionObserver === 'function'
? new IntersectionObserver(handleVisibilityChange)
: null
);
const anchorRef = useRef();
const targetRef = useRef();
useEffect(() => {
if (anchorRef.current) {
targetRef.current = anchorRef.current.nextElementSibling;
setRenderAnchor(false);
if (targetRef.current) {
io?.observe(targetRef.current);
}
}
}, [io]);
useEffect(() => {
return () => {
if (targetRef.current) {
io?.unobserve(targetRef.current);
}
io?.disconnect();
};
}, [io]);
function handleVisibilityChange(changes) {
const change = changes[0];
if (change.intersectionRatio > 0) {
io?.unobserve(change.target);
io?.disconnect();
if (traceId) {
analytics.track(traceId, traceData);
}
}
}
return (
<>
{renderAnchor && (
<span
ref={anchorRef}
style={{
position: 'absolute',
visibility: 'hidden',
pointerEvents: 'none',
width: 0,
height: 0,
overflow: 'hidden'
}}
/>
)}
{children}
</>
);
}
Final Reflections
This journey of technical evolution taught me several important lessons:
There are no perfect solutions, only solutions that fit the current context. The wrapper approach, despite its flaws, might still be the most straightforward choice in certain scenarios.
Technology updates are double-edged swords. The React team had valid reasons for removing findDOMNode
, but it also created migration costs for existing projects.
Innovation often emerges from desperation. When conventional paths are blocked, we’re forced to find more creative solutions.
Real technical growth comes from solving real problems. It’s not about memorizing APIs, but about finding viable solutions when facing concrete challenges.
Now, whenever junior developers ask me about exposure tracking implementation, I share this story with them. Not to show off technical skills, but to help them understand that the path of technology is never smooth sailing. It’s these setbacks and breakthroughs that make us better engineers.
After all, code becomes obsolete, frameworks get updated, but the mindset for problem-solving is eternal.
This content originally appeared on DEV Community and was authored by reactuse.com