For a long time, I know useRef.
If you had asked me month ago, I would have said it's just for accessing DOM elements. And honestly, that's what most of us believe. We reach for useRef when we need to focus an input or scroll to a section, and then we move on.
That understanding isn't wrong. It's just incomplete.
useRef is not a weaker version of useState, and it's not limited to DOM access. It exists for a very specific class of problems the kind that state and effects struggle with. Things like imperative logic, timing issues, browser APIs, and stale closures.
What useRef Actually Gives You
At its core, useRef is very simple.
const ref = useRef<HTMLDivElement | null>(null);const ref = useRef<HTMLDivElement | null>(null);
This returns an object with a single property:
{current: initialValue;}{current: initialValue;}
That object stays the same across every re-render of the component. You can change ref.current as much as you want, and React will not re-render the component because of it.
That's the whole point.
useRef gives you a place to store mutable data that belongs to a component instance but has nothing to do with rendering.
Why DOM Access Is the Most Common Use Case
Most people first encounter useRef through DOM access, and for good reason.
function Search() {const inputRef = useRef(null);useEffect(() => {inputRef.current?.focus();}, []);return <input ref={inputRef} />;}function Search() {const inputRef = useRef(null);useEffect(() => {inputRef.current?.focus();}, []);return <input ref={inputRef} />;}
This works because focus is not a UI state. It's a browser action. React doesn't control focus the browser does.
Trying to represent things like focus, scroll position, or cursor location with state usually creates more problems than it solves. React is declarative, but the browser is fundamentally imperative. Some things need to be told what to do, not described.
That's where using useRef for DOM access makes sense.
Trying to model these with state is a mistake. React is declarative, but the browser is not.
Declarative Programming (React's World)
Example:
{isOpen && <Modal />;}{isOpen && <Modal />;}
In React, you don't write step-by-step instructions for the DOM. You simply describe what the UI should look like for a given state, and React handles the rest. You never manually create elements, insert them, or wire events that's React's job.
This is why React is called declarative: you focus on the result, not the process.
Imperative Programming (Browser's World)
Imperative code answers a different question:
"Do this specific action right now."
Example:
element.focus();element.scrollIntoView();element.getBoundingClientRect();element.focus();element.scrollIntoView();element.getBoundingClientRect();
You are issuing commands to an already-existing object.
Why this matters: React only controls rendering part of the ui but your browser is responsible for controlling physical behavior of it e.g: window height-width, focus element, modifying dom attributes directly.
Storing Non-React Values That Should Persist Without Re-Rendering
This is where useRef already becomes more powerful than most people realize.
Some values need to survive re-renders, change over time and not cause UI updates. few examples are like timer's, observers, websocket connections.
function Ticker() {const timerRef = useRef(null);useEffect(() => {timerRef.current = setInterval(() => {console.log("tick");}, 1000);return () => clearInterval(timerRef.current);}, []);return null;}function Ticker() {const timerRef = useRef(null);useEffect(() => {timerRef.current = setInterval(() => {console.log("tick");}, 1000);return () => clearInterval(timerRef.current);}, []);return null;}
This is exactly what useRef is meant for: mutable instance data.
Why useEffect Starts Causing Problems With DOM Measurements
Sooner or later, you hit this situation:
"I need to render something, measure it, then adjust layout."
You try useEffect.
What Actually Happens With useEffect
react-lifecycle
useEffecct-lifecycleThis means the user already saw the UI. Any DOM measurement happens after UI painting is done. if we are manupulating anything after that paint we see visible layout shift in UI ( this create bad User Experience ).
The flicker you see that moment ( when layout shift happen ) is not because react is slow in measuring. it's upto us how we are measuring and updating the dom.
One solution to prevent this is to use useLayoutEffect hook which runs before painting the ui in browser. But it's not always the right way to fix these flicker issue. let me explain in detail and how to it in better way.
Why useLayoutEffect Is Not Always the Right Fix
useLayoutEffect runs before paint, so it avoids flicker.
But it also:
- blocks rendering
- can hurt performance
- should not be abused
This is where callback refs become the cleaner solution.
Callback Refs: Accessing the DOM at the Exact Right Time
callback-ref-lifecycleInstead of passing a ref object, you pass a function.
function Box() {const setNode = (node) => {if (!node) return;const rect = node.getBoundingClientRect();console.log(rect);};return <div ref={setNode}>box</div>;}function Box() {const setNode = (node) => {if (!node) return;const rect = node.getBoundingClientRect();console.log(rect);};return <div ref={setNode}>box</div>;}
What's a difference between passing callback or ref object?
combine-ref-with-callback-and-without-callbackIn both above scenarios the work will be same only the difference will be how lifecycle of painting and doing side effect will change the overall experience.
The Problem: State Closures in React
Every developer face issue called "State Closures" in React state. This to overcome with this we can use useRef hook to get latest data always.
import { useEffect, useState } from "react";function Editor() {const [content, setContent] = useState("");useEffect(() => {const id = setInterval(() => {// content is always old heresaveDraft(content);}, 5000);return () => clearInterval(id);}, []);return (<textareavalue={content}onChange={(e) => setContent(e.target.value)}placeholder="start typing..."/>);}import { useEffect, useState } from "react";function Editor() {const [content, setContent] = useState("");useEffect(() => {const id = setInterval(() => {// content is always old heresaveDraft(content);}, 5000);return () => clearInterval(id);}, []);return (<textareavalue={content}onChange={(e) => setContent(e.target.value)}placeholder="start typing..."/>);}
This logs old state forever.
This is not a React bug. This is how JavaScript closures work.
The Fix: use Refs as "Latest Value Holders"
import { useEffect, useRef, useState } from "react";function Editor() {const [content, setContent] = useState("");const contentRef = useRef(content);// keep ref in sync with latest stateuseEffect(() => {contentRef.current = content;}, [content]);// stable intervaluseEffect(() => {const id = setInterval(() => {// ✅ always latest valuesaveDraft(contentRef.current);}, 5000);return () => clearInterval(id);}, []); // interval created oncereturn (<textareavalue={content}onChange={(e) => setContent(e.target.value)}placeholder="start typing..."/>);}import { useEffect, useRef, useState } from "react";function Editor() {const [content, setContent] = useState("");const contentRef = useRef(content);// keep ref in sync with latest stateuseEffect(() => {contentRef.current = content;}, [content]);// stable intervaluseEffect(() => {const id = setInterval(() => {// ✅ always latest valuesaveDraft(contentRef.current);}, 5000);return () => clearInterval(id);}, []); // interval created oncereturn (<textareavalue={content}onChange={(e) => setContent(e.target.value)}placeholder="start typing..."/>);}
Why This Fix Works (Step by Step)
State updates trigger re-render (normal) Ref is updated with the latest value Interval callback always reads ref.current Interval never restarts Latest data is always saved
Common Misconceptions
Common misconceptions that cause bugs in your application and can reduce application performance.
Updating a refs does not update UI. Refs are not only for the DOM access/manipulation.
Final Takeaway
useRef is not a state. it's not an effect also it not a DOM shortcut.
It is stable escape hatch for imperative logic, external systems, timing-sensitive code and stale clousures.