š§ 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
. š
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.
ref
: Getting Access to the DOMReact 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 (<divref={(myDiv) => {if (!myDiv) returnconsole.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 benull
, so you should guard against that.
useRef
HookFor 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.
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.currentVanillaTilt.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>}
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.currentVanillaTilt.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?
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.
Instead of passing a full object into your useEffect
dependency array, pull out the primitive values directly:
useEffect(() => {const tiltNode = tiltRef.currentVanillaTilt.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.
Refs are often used for DOM access, but theyāre just persistent containers:
const myRef = useRef(initialValue)
myRef.current
holds any value you wantmyRef.current
does not trigger a re-renderThis makes refs a good fit for storing mutable state that doesnāt affect the visual outputālike timers, previous props, or external library instances.
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.
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.
Quick Links
Legal Stuff
Social Media