Hey there, Fellow Coder! đź‘‹
We’ve talked about useState (the loud one that updates the UI) and useEffect (the one that reacts to changes). Now, let’s talk about the silent ninja of React hooks: useRef.
If useState is like a news anchor shouting “BREAKING NEWS!” every time something changes, useRef is like a spy taking notes in a corner. It knows things changed, but it doesn’t make a scene.
Let’s break down why you need this “Silent Observer”. 🕵️‍♂️
What is useRef actually?
In simple terms, useRef creates a plain JavaScript object that holds a value.
It looks like this:
{ current: ... }
That’s it. It’s just a box with a current property.
But here’s the superpower: Changing ref.current does NOT trigger a re-render.
This makes it perfect for two things:
- Accessing DOM elements directly.
- Storing values that persist across renders but don’t need to be shown on screen.
1. Accessing the DOM (The Most Common Use)
Sometimes React’s virtual DOM isn’t enough. You need to grab the actual HTML element to focus an input, scroll to a div, or measure an element’s size.
Here is the classic “Focus Input” example:
import { useRef, useEffect } from "react";
const SearchBar = () => {
const inputRef = useRef(null); // 1. Create the ref
const handleFocus = () => {
// 3. Access the DOM node using .current
inputRef.current.focus();
};
return (
<div>
{/* 2. Attach the ref to the element */}
<input ref={inputRef} type="text" placeholder="Search..." />
<button onClick={handleFocus}>Focus the Input</button>
</div>
);
};
What happened here?
When React creates the <input>, it puts the actual DOM node into inputRef.current. Now you can call standard DOM methods like .focus(), .scrollIntoView(), etc.
2. Storing Mutable Variables (The “Secret” Memory)
This is the use case that often confuses people. Imagine you want to track how many times a button was clicked, but you don’t want to re-render the component just to show that number (maybe you send it to analytics later).
If you use useState, the component re-renders every click.
If you use a normal variable (let count = 0), it resets to 0 every re-render.
Enter useRef.
import { useRef, useState } from "react";
const ClickTracker = () => {
const [dummyState, setDummyState] = useState(0);
const clickCount = useRef(0); // Initialize with 0
const handleClick = () => {
clickCount.current = clickCount.current + 1;
console.log(`Button clicked ${clickCount.current} times`);
};
return (
<div>
<button onClick={handleClick}>Click Me (Check Console)</button>
{/* This button just forces a re-render to prove the ref persists */}
<button onClick={() => setDummyState(dummyState + 1)}>
Force Re-render
</button>
</div>
);
};
Try this mentally:
- Click “Click Me” 5 times. The console says “5”. The screen DOES NOT update.
- Click “Force Re-render”. The component re-runs.
clickCount.currentis STIlL 5. It didn’t reset!
The Golden Rule ⚠️
Do NOT read or write ref.current during rendering.
React expects rendering to be “pure”. Reading/writing refs during render makes it unpredictable.
// ❌ BAD
const Component = () => {
const count = useRef(0);
count.current = count.current + 1; // Writing during render!
return <h1>{count.current}</h1>; // Reading during render!
};
// âś… GOOD
const Component = () => {
const count = useRef(0);
useEffect(() => {
count.current = count.current + 1; // Writing in Effect is fine
});
return <h1>{count.current}</h1>; // (Still risky to read if it changes often, prefer State for UI)
};
If you want to show something on the screen, use useState.
If you want to store something “behind the scenes”, use useRef.
Summary
Think of your component as a person working in an office.
useState: Is their outfit. If they change it, everyone notices (Re-render).useRef: Is a note in their pocket. They can update it, read it, and keep it safe, but nobody else sees it changing.
Use useRef when you need to step out of the React flow (DOM access) or keep secrets from the UI (mutable variables).
Happy Coding! 🚀