❓What is useRef?
The useRef hook in React is a versatile and powerful tool primarily used to persist values across component renders and to directly reference and manipulate DOM elements without causing re-renders.
useRefreturns a mutable reference object whose.currentproperty holds a value that remains consistent across the lifetime of the component.It is mainly used for:
Accessing DOM elements directly, such as focusing an input or altering styles.
Storing mutable values that you want to persist between renders but don't want to trigger a component re-render when they change.
Characteristics
The returned ref object has a shape:
{ current: initialValue }.The initial value is set once during the first render and ignored in subsequent renders.
Changing
ref.currentdoes not cause a re-render, unlike updating state.The object returned by
useRefis stable — it's the same object on every render.
🧐 Why use useRef instead of state?
State changes cause re-rendering of the component, but refs do not.
Use
useRefto store data that does not need to impact rendering (for example, to store timers, previous values, or mutable variables).Ideal for cases where you want to read or write a value directly, but where UI should not update when it changes.
✅ Common Use Cases of useRef
1. Accessing and manipulating DOM elements
Assign
useRefto a JSX element'srefprop to get a direct handle on the real DOM nodeCommon for imperative actions React doesn't have a declarative prop for — focusing an input, scrolling to an element, measuring size, playing a video
Keeps DOM access inside React's world instead of falling back to
document.querySelector
2. Persisting mutable values without causing re-renders
A ref's
.currentcan be changed freely and it survives across renders — but changing it never schedules a re-renderGood for values the UI doesn't need to reflect immediately: counters for internal bookkeeping, timers/interval IDs, render-cycle counts, flags
Trade-off: if you do need the UI to update when the value changes, you want
useState, notuseRef
3. Preventing an effect from running on the initial mount
A ref (e.g.
isFirstRender) can act as a one-time flag your effect checks and flipsLets you skip the effect's logic on mount and only run it on genuine updates — useful when the effect should react to changes, not to the component just appearing
4. Storing "stale" data or previous props/state
Update a ref inside an effect to snapshot the current value right after each render commits
Since the ref update happens after render, reading
.currentduring the next render gives you the previous value — a simple way to build "before vs. now" comparisons without extra state
<div class="infographic-userefcases" id="rc">
<style>
.infographic-userefcases {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--line: #ddd6c9;
--accent: #2563eb;
--radius: 8px;
--mono-font: 'IBM Plex Mono', monospace;
--body-font: 'Inter', system-ui, sans-serif;
max-width: 720px;
margin: 0 auto;
box-sizing: border-box;
background: transparent;
color: var(--ink);
font-family: var(--body-font);
}
.infographic-userefcases[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--line: #2c313d;
--accent: #5b8def;
}
.infographic-userefcases * { box-sizing: border-box; min-width: 0; }
.rc-tabs { display: flex; justify-content: center; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1.5rem; }
.rc-tab-btn {
font: inherit; font-size: 0.78rem; font-weight: 600; color: var(--ink-soft); background: transparent;
border: 1px solid var(--line); border-radius: 100px; padding: 0.45rem 0.9rem; cursor: pointer;
transition: background 0.2s, color 0.2s, border-color 0.2s;
}
.rc-tab-btn.is-active { background: var(--accent); border-color: var(--accent); color: #fff; }
.rc-tab-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.rc-section { margin-bottom: 2.4rem; display: none; }
.rc-section.is-active { display: block; }
.rc-section:last-child { margin-bottom: 0; }
.rc-section-label {
font-family: var(--mono-font); font-size: 0.72rem; font-weight: 700; color: var(--accent);
margin: 0 0 0.6rem; letter-spacing: 0.02em;
}
.rc-grid { display: flex; flex-direction: column; gap: 1.1rem; align-items: stretch; }
.rc-code-block pre {
margin: 0; background: #0d0d10; border: 1px solid #24252d; border-radius: var(--radius);
padding: 0.9rem; overflow: auto; max-height: 360px;
}
.rc-code-block code { font-family: var(--mono-font); font-size: 0.74rem; line-height: 1.72; color: #d4d4d8; white-space: pre; }
.tok-kw { color: #c678dd; }
.tok-tag { color: #e06c75; }
.tok-type { color: #61afef; }
.tok-attr { color: #d19a66; }
.tok-str { color: #98c379; }
.tok-expr { color: #56b6c2; }
.tok-punc { color: #6b7280; }
.tok-comment { color: #7f848e; font-style: italic; }
.rc-output-label {
font-family: var(--mono-font); font-size: 0.68rem; letter-spacing: 0.1em; text-transform: uppercase;
color: var(--ink-soft); text-align: center;
}
.rc-preview { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; box-shadow: 0 10px 24px -12px rgba(26,26,26,0.18); }
.rc-preview-bar { background: #e9e9e9; padding: 0.45rem 0.6rem; display: flex; gap: 0.3rem; }
.rc-preview-bar span { width: 8px; height: 8px; border-radius: 50%; display: block; }
.rc-preview-bar span:nth-child(1) { background: #ff5f56; }
.rc-preview-bar span:nth-child(2) { background: #ffbd2e; }
.rc-preview-bar span:nth-child(3) { background: #27c93f; }
.rc-preview-page { background: #fff; padding: 1.6rem; color: #000; text-align: center; }
.rc-real-input { font-family: var(--body-font); font-size: 0.9rem; padding: 0.5rem 0.7rem; border: 1px solid #ccc; border-radius: 5px; color: #000; }
.rc-real-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(37,99,235,0.25); }
.rc-real-btn {
font-family: var(--body-font); font-size: 0.85rem; font-weight: 600; color: #fff; background: var(--accent);
border: none; border-radius: 100px; padding: 0.55rem 1.1rem; cursor: pointer; transition: transform 0.15s;
}
.rc-real-btn:hover { transform: translateY(-1px); }
.rc-console { background: #0d0d10; border-radius: 6px; padding: 0.6rem 0.8rem; text-align: left; margin-top: 1rem; max-height: 120px; overflow-y: auto; }
.rc-line { font-family: var(--mono-font); font-size: 0.74rem; line-height: 1.7; color: #98c379; opacity: 0; animation: rc-fade 0.3s ease forwards; }
.rc-line.rc-muted { color: #6b7280; font-style: italic; }
@keyframes rc-fade { from { opacity: 0; transform: translateY(-2px); } to { opacity: 1; transform: translateY(0); } }
.rc-now-before { font-family: var(--mono-font); font-size: 1.1rem; margin-bottom: 0.3rem; }
.rc-now-before.rc-before { color: var(--ink-soft); font-size: 0.95rem; margin-bottom: 1rem; }
@media (prefers-reduced-motion: reduce) {
.infographic-userefcases *, .infographic-userefcases *::before, .infographic-userefcases *::after {
transition-duration: 0.001ms !important; animation-duration: 0.001ms !important;
}
}
</style>
<div class="rc-tabs">
<button class="rc-tab-btn is-active" data-tab="1">DOM Focus</button>
<button class="rc-tab-btn" data-tab="2">No Re-render</button>
<button class="rc-tab-btn" data-tab="3">Skip on Mount</button>
<button class="rc-tab-btn" data-tab="4">Previous Value</button>
</div>
<!-- 1. Focus on mount -->
<div class="rc-section is-active" data-tab-panel="1">
<p class="rc-section-label">1. Accessing and Manipulating DOM Elements</p>
<div class="rc-grid">
<div class="rc-code-block"><pre><code id="rc1Code"></code></pre></div>
<p class="rc-output-label">OUTPUT</p>
<div class="rc-preview">
<div class="rc-preview-bar"><span></span><span></span><span></span></div>
<div class="rc-preview-page">
<input class="rc-real-input" type="text" id="rc1Input" />
</div>
</div>
</div>
</div>
<!-- 2. Mutable ref, no re-render -->
<div class="rc-section" data-tab-panel="2">
<p class="rc-section-label">2. Persisting Mutable Values without Causing Re-Renders</p>
<div class="rc-grid">
<div class="rc-code-block"><pre><code id="rc2Code"></code></pre></div>
<p class="rc-output-label">OUTPUT</p>
<div class="rc-preview">
<div class="rc-preview-bar"><span></span><span></span><span></span></div>
<div class="rc-preview-page">
<button class="rc-real-btn" id="rc2Btn" type="button">Increase Count 0</button>
<div class="rc-console" id="rc2Console"></div>
</div>
</div>
</div>
</div>
<!-- 3. Skip effect on first render -->
<div class="rc-section" data-tab-panel="3">
<p class="rc-section-label">3. Preventing Effects on the Initial Mount</p>
<div class="rc-grid">
<div class="rc-code-block"><pre><code id="rc3Code"></code></pre></div>
<p class="rc-output-label">OUTPUT</p>
<div class="rc-preview">
<div class="rc-preview-bar"><span></span><span></span><span></span></div>
<div class="rc-preview-page">
<button class="rc-real-btn" id="rc3Btn" type="button">Change someDependency</button>
<div class="rc-console" id="rc3Console"></div>
</div>
</div>
</div>
</div>
<!-- 4. Track previous value -->
<div class="rc-section" data-tab-panel="4">
<p class="rc-section-label">4. Storing Stale Data / Previous Props</p>
<div class="rc-grid">
<div class="rc-code-block"><pre><code id="rc4Code"></code></pre></div>
<p class="rc-output-label">OUTPUT</p>
<div class="rc-preview">
<div class="rc-preview-bar"><span></span><span></span><span></span></div>
<div class="rc-preview-page">
<div class="rc-now-before" id="rc4Now">Now: 0</div>
<div class="rc-now-before rc-before" id="rc4Before">Before: N/A</div>
<button class="rc-real-btn" id="rc4Btn" type="button">Increment</button>
</div>
</div>
</div>
</div>
</div>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<script>
(function () {
var root = document.getElementById('rc');
if (!root) return;
var bodyAttr = document.body.getAttribute('data-darkmode');
var dark;
if (bodyAttr === 'true') dark = true;
else if (bodyAttr === 'false') dark = false;
else {
var match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
if (dark) root.setAttribute('data-theme', 'dark');
window.addEventListener('message', function (e) {
if (e.data && e.data.type === 'darkMode') {
if (e.data.value) root.setAttribute('data-theme', 'dark');
else root.removeAttribute('data-theme');
}
});
function logLine(container, text, cls) {
var div = document.createElement('div');
div.className = 'rc-line' + (cls ? ' ' + cls : '');
div.textContent = text;
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
/* ===== 1. Input focus on mount ===== */
root.querySelector('#rc1Code').innerHTML =
'<span class="tok-kw">import</span> <span class="tok-type">React</span><span class="tok-punc">,</span> <span class="tok-punc">{</span> useRef<span class="tok-punc">,</span> useEffect <span class="tok-punc">}</span> <span class="tok-kw">from</span> <span class="tok-str">\'react\'</span><span class="tok-punc">;</span>\n\n' +
'<span class="tok-kw">function</span> <span class="tok-type">InputFocus</span><span class="tok-punc">() {</span>\n' +
' <span class="tok-kw">const</span> inputRef <span class="tok-punc">=</span> useRef<span class="tok-punc">(</span><span class="tok-kw">null</span><span class="tok-punc">);</span>\n\n' +
' useEffect<span class="tok-punc">(() => {</span>\n' +
' <span class="tok-comment">// Access DOM node directly and set focus on page load/render</span>\n' +
' inputRef.current.focus<span class="tok-punc">();</span>\n' +
' <span class="tok-punc">},</span> <span class="tok-punc">[]);</span>\n\n' +
' <span class="tok-kw">return</span> <span class="tok-punc"><</span><span class="tok-tag">input</span> <span class="tok-attr">ref</span><span class="tok-punc">={</span>inputRef<span class="tok-punc">}</span> <span class="tok-attr">type</span><span class="tok-punc">=</span><span class="tok-str">"text"</span> <span class="tok-punc">/>;</span>\n' +
'<span class="tok-punc">}</span>';
function initSection1() {
// real behavior: same as inputRef.current.focus() in useEffect(..., [])
setTimeout(function () { root.querySelector('#rc1Input').focus(); }, 200);
}
/* ===== 2. Mutable ref, no re-render ===== */
root.querySelector('#rc2Code').innerHTML =
'<span class="tok-kw">import</span> <span class="tok-type">React</span><span class="tok-punc">,</span> <span class="tok-punc">{</span> useRef <span class="tok-punc">}</span> <span class="tok-kw">from</span> <span class="tok-str">\'react\'</span><span class="tok-punc">;</span>\n\n' +
'<span class="tok-kw">function</span> <span class="tok-type">Timer</span><span class="tok-punc">() {</span>\n' +
' <span class="tok-kw">const</span> countRef <span class="tok-punc">=</span> useRef<span class="tok-punc">(</span><span class="tok-expr">0</span><span class="tok-punc">);</span>\n\n' +
' <span class="tok-kw">const</span> handleClick <span class="tok-punc">=</span> <span class="tok-punc">() => {</span>\n' +
' countRef.current<span class="tok-punc">++;</span>\n' +
' console.log<span class="tok-punc">(</span><span class="tok-str">\'Count value:\'</span><span class="tok-punc">,</span> countRef.current<span class="tok-punc">);</span>\n' +
' <span class="tok-comment">// Logs updated value but does not re-render component</span>\n' +
' <span class="tok-punc">};</span>\n\n' +
' <span class="tok-kw">return</span> <span class="tok-punc"><</span><span class="tok-tag">button</span> <span class="tok-attr">onClick</span><span class="tok-punc">={</span>handleClick<span class="tok-punc">}></span>Increase Count <span class="tok-punc">{</span>countRef.current<span class="tok-punc">}</</span><span class="tok-tag">button</span><span class="tok-punc">>;</span>\n' +
'<span class="tok-punc">}</span>';
function initSection2() {
var countRefCurrent = 0;
var btn = root.querySelector('#rc2Btn');
var console_ = root.querySelector('#rc2Console');
btn.addEventListener('click', function () {
countRefCurrent += 1;
logLine(console_, 'Count value: ' + countRefCurrent);
// button text is NOT updated — matches the real bug: ref changes don't trigger a re-render,
// so the JSX with countRef.current stays stuck at whatever it was on the last real render.
});
}
/* ===== 3. Skip effect on first render ===== */
root.querySelector('#rc3Code').innerHTML =
'<span class="tok-kw">const</span> isFirstRender <span class="tok-punc">=</span> useRef<span class="tok-punc">(</span><span class="tok-kw">true</span><span class="tok-punc">);</span>\n\n' +
'useEffect<span class="tok-punc">(() => {</span>\n' +
' <span class="tok-kw">if</span> <span class="tok-punc">(</span>isFirstRender.current<span class="tok-punc">) {</span>\n' +
' isFirstRender.current <span class="tok-punc">=</span> <span class="tok-kw">false</span><span class="tok-punc">;</span> <span class="tok-comment">// Mark that we\'ve finished the first render</span>\n' +
' <span class="tok-kw">return</span><span class="tok-punc">;</span>\n' +
' <span class="tok-punc">}</span>\n' +
' console.log<span class="tok-punc">(</span><span class="tok-str">"This only runs on updates, not on mount!"</span><span class="tok-punc">);</span>\n' +
'<span class="tok-punc">},</span> <span class="tok-punc">[</span>someDependency<span class="tok-punc">]);</span>';
function initSection3() {
var isFirstRender = true;
var btn = root.querySelector('#rc3Btn');
var console_ = root.querySelector('#rc3Console');
logLine(console_, '// mounted — effect ran, but skipped the log (isFirstRender was true)', 'rc-muted');
btn.addEventListener('click', function () {
if (isFirstRender) {
isFirstRender = false; // already handled above, kept for parity with the real code path
}
logLine(console_, 'This only runs on updates, not on mount!');
});
}
/* ===== 4. Track previous value ===== */
root.querySelector('#rc4Code').innerHTML =
'<span class="tok-kw">import</span> <span class="tok-type">React</span><span class="tok-punc">,</span> <span class="tok-punc">{</span> useState<span class="tok-punc">,</span> useEffect<span class="tok-punc">,</span> useRef <span class="tok-punc">}</span> <span class="tok-kw">from</span> <span class="tok-str">\'react\'</span><span class="tok-punc">;</span>\n\n' +
'<span class="tok-kw">function</span> <span class="tok-type">TrackPrevious</span><span class="tok-punc">() {</span>\n' +
' <span class="tok-kw">const</span> <span class="tok-punc">[</span>count<span class="tok-punc">,</span> setCount<span class="tok-punc">]</span> <span class="tok-punc">=</span> useState<span class="tok-punc">(</span><span class="tok-expr">0</span><span class="tok-punc">);</span>\n' +
' <span class="tok-kw">const</span> prevCountRef <span class="tok-punc">=</span> useRef<span class="tok-punc">();</span>\n\n' +
' <span class="tok-comment">// This runs AFTER the render is committed to the screen</span>\n' +
' useEffect<span class="tok-punc">(() => {</span>\n' +
' prevCountRef.current <span class="tok-punc">=</span> count<span class="tok-punc">;</span>\n' +
' <span class="tok-punc">},</span> <span class="tok-punc">[</span>count<span class="tok-punc">]);</span>\n\n' +
' <span class="tok-comment">// We capture the value of the ref BEFORE the effect updates it</span>\n' +
' <span class="tok-kw">const</span> prevCount <span class="tok-punc">=</span> prevCountRef.current<span class="tok-punc">;</span>\n\n' +
' <span class="tok-kw">return</span> <span class="tok-punc">(</span>\n' +
' <span class="tok-punc"><</span><span class="tok-tag">div</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc"><</span><span class="tok-tag">h1</span><span class="tok-punc">></span>Now<span class="tok-punc">:</span> <span class="tok-punc">{</span>count<span class="tok-punc">}</</span><span class="tok-tag">h1</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc"><</span><span class="tok-tag">h2</span><span class="tok-punc">></span>Before<span class="tok-punc">:</span> <span class="tok-punc">{</span>prevCount <span class="tok-punc">!==</span> <span class="tok-kw">undefined</span> <span class="tok-punc">?</span> prevCount <span class="tok-punc">:</span> <span class="tok-str">"N/A"</span><span class="tok-punc">}</</span><span class="tok-tag">h2</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc"><</span><span class="tok-tag">button</span> <span class="tok-attr">onClick</span><span class="tok-punc">={</span><span class="tok-punc">() =></span> setCount<span class="tok-punc">(</span>count <span class="tok-punc">+</span> <span class="tok-expr">1</span><span class="tok-punc">)}></span>\n' +
' Increment\n' +
' <span class="tok-punc"></</span><span class="tok-tag">button</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc"></</span><span class="tok-tag">div</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc">);</span>\n' +
'<span class="tok-punc">}</span>';
function initSection4() {
var count = 0;
var prevCount; // undefined until the effect runs once
var nowEl = root.querySelector('#rc4Now');
var beforeEl = root.querySelector('#rc4Before');
var btn = root.querySelector('#rc4Btn');
function render() {
nowEl.textContent = 'Now: ' + count;
beforeEl.textContent = 'Before: ' + (prevCount !== undefined ? prevCount : 'N/A');
}
render();
btn.addEventListener('click', function () {
count += 1;
render(); // render happens first, showing the OLD prevCount alongside the NEW count
// effect runs AFTER the render commits — updates the ref for next time
setTimeout(function () { prevCount = count; }, 0);
});
}
/* ===== tab switching (lazy-init each demo on first visit) ===== */
var initFns = { 1: initSection1, 2: initSection2, 3: initSection3, 4: initSection4 };
var initialized = {};
var tabBtns = Array.prototype.slice.call(root.querySelectorAll('.rc-tab-btn'));
var panels = Array.prototype.slice.call(root.querySelectorAll('.rc-section'));
function activateTab(id) {
tabBtns.forEach(function (b) { b.classList.toggle('is-active', b.getAttribute('data-tab') === id); });
panels.forEach(function (p) { p.classList.toggle('is-active', p.getAttribute('data-tab-panel') === id); });
if (!initialized[id]) {
initialized[id] = true;
initFns[id]();
}
}
tabBtns.forEach(function (b) {
b.addEventListener('click', function () { activateTab(b.getAttribute('data-tab')); });
});
activateTab('1');
})();
</script>✏️ Working with Refs in React
Use the
refattribute on any JSX element to attach the ref.React sets the
.currentproperty of the ref object to the DOM element after render.When the element unmounts, React updates the ref's
.currenttonull.You can update
.currentmanually for any data you want to persist without rendering.
Summary of Key Points
Aspect | Details |
|---|---|
Returned Value | Mutable ref object with |
Persistence | Value persists through renders |
Causes Re-renders? | No, updating |
Typical Use Cases | Access DOM elements, persist mutable values without rendering |
Initial Value Set On | Only on initial render |
Changing | At any time without affecting the render cycle |