HomeAbout Me

React Hooks: DOM Side-Effects

By Daniel Nguyen
Published in React JS
June 05, 2025
2 min read
React Hooks: DOM Side-Effects

🧠 Understanding DOM Side-Effects and ref in React

In React, your job is to describe what the UI should look like—not how it should be rendered. But what happens when you do need to work with the DOM directly? Whether you’re integrating with a third-party JavaScript library or managing focus manually, React gives you tools to perform side-effects while respecting its rendering model.

In this article, we’ll explore DOM side-effects, how to use ref and useEffect properly, and how to avoid some common pitfalls around dependencies. We’ll even build a fun interactive component using vanilla-tilt. šŸŽ‰


🧩 Why Can’t We Access the DOM in Render?

Let’s start with a basic principle: React’s render methods do not create DOM nodes. Instead, they return React Elements—which are like blueprints. Only when ReactDOM.createRoot().render(...) is called do those blueprints get turned into actual DOM nodes.

This means:

return <div>Hi</div>

…doesn’t give you access to the <div> DOM node—only a React Element object.


šŸŽÆ Enter ref: Getting Access to the DOM

React provides a special prop called ref that lets you access the underlying DOM node once it exists.

There are two main ways to use ref:

function MyDiv() {
return (
<div
ref={(myDiv) => {
if (!myDiv) return
console.log('Here’s my div!', myDiv)
}}
>
Hey, this is my div!
</div>
)
}

šŸ” This ref is a function that gets called after the DOM element is mounted or updated.

āš ļø With TypeScript, the myDiv parameter may be null, so you should guard against that.


2. Object Ref with useRef Hook

For more advanced or reusable DOM interactions, you can use the useRef hook:

import { useRef, useEffect } from 'react'
function MyDiv() {
const myDivRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (myDivRef.current) {
console.log('My DOM node:', myDivRef.current)
}
}, [])
return <div ref={myDivRef}>hi</div>
}

This lets you persist a reference to a DOM node across re-renders without triggering additional renders.


šŸŒ€ A Real Example: Fancy Tilt Effect

Let’s say you’re building a <Tilt /> component using vanilla-tilt. That library expects a real DOM node and attaches event listeners to it. Since React owns the node creation, we need to ask React to give it to us using a ref.

function Tilt() {
const tiltRef = useRef(null)
useEffect(() => {
const tiltNode = tiltRef.current
VanillaTilt.init(tiltNode, {
max: 25,
speed: 400,
glare: true,
"max-glare": 0.5,
})
return () => {
tiltNode.vanillaTilt?.destroy()
}
}, [])
return <div ref={tiltRef} className="tilt-root">Fancy Tilt ✨</div>
}

šŸ” Problem: My Tilt Keeps Resetting!

Let’s say you enhance your component with user controls so people can change tilt options like speed and glare. You now pass those options into the component, and update the effect like this:

useEffect(() => {
const tiltNode = tiltRef.current
VanillaTilt.init(tiltNode, options)
return () => tiltNode.vanillaTilt?.destroy()
}, [options])

This seems correct—but the tilt effect resets every time you click the button—even if the options didn’t actually change. Why?


🧠 Dependency Gotcha: Object Identity

The problem is subtle: the options object is recreated every time the component renders. So even if its values are the same, it’s a different object in memory.

const a = { speed: 400 }
const b = { speed: 400 }
console.log(Object.is(a, b)) // false

Since useEffect checks dependencies using Object.is, it treats options as ā€œchangedā€ every render—even if values are unchanged. That’s why the effect keeps running.


āœ… The Fix: Use Primitive Dependencies

Instead of passing a full object into your useEffect dependency array, pull out the primitive values directly:

useEffect(() => {
const tiltNode = tiltRef.current
VanillaTilt.init(tiltNode, {
glare,
max,
speed,
'max-glare': maxGlare,
})
return () => tiltNode.vanillaTilt?.destroy()
}, [glare, max, speed, maxGlare])

This ensures the effect only reruns when actual values change—not just when object references change.


šŸ’” Bonus: What is a Ref Anyway?

Refs are often used for DOM access, but they’re just persistent containers:

  • const myRef = useRef(initialValue)
  • myRef.current holds any value you want
  • Updating myRef.current does not trigger a re-render

This makes refs a good fit for storing mutable state that doesn’t affect the visual output—like timers, previous props, or external library instances.


šŸ“š Summary

React’s declarative nature doesn’t stop you from performing imperative tasks—you just need to do them the React way. Here’s what we covered:

āœ… Use ref to access the DOM. āœ… Use useEffect to safely perform setup and cleanup side-effects. āœ… Use primitive dependencies in useEffect to avoid unnecessary reruns. āœ… Avoid updating DOM or initializing effects inside render methods.


šŸ”— Further Reading


Ready to tilt your way into DOM mastery? Give it a try! šŸ§™ā€ā™‚ļøāœØ

If you found this helpful, share it with someone learning React, or follow along for deeper dives into React’s more advanced hooks and behaviors.



Tags

#ReactHooks

Share

Previous Article
React Hooks: Lifting State

Table Of Contents

1
🧩 Why Can't We Access the DOM in Render?
2
šŸŽÆ Enter ref: Getting Access to the DOM
3
šŸŒ€ A Real Example: Fancy Tilt Effect
4
šŸ” Problem: My Tilt Keeps Resetting!
5
🧠 Dependency Gotcha: Object Identity
6
āœ… The Fix: Use Primitive Dependencies
7
šŸ’” Bonus: What is a Ref Anyway?
8
šŸ“š Summary
9
šŸ”— Further Reading

Related Posts

React Hook: Tic Tac Toe
June 07, 2025
2 min
Ā© 2025, All Rights Reserved.
Powered By

Quick Links

About Me

Legal Stuff

Social Media