-
useRefvalues are mutable containers that persist across renders but do not trigger re-renders when changed. -
ref.currentis like editing a normal JS object property that React does not track for re-render triggers. -
Refs are intentionally designed as “escape hatches” — minimum overhead, no reconciliation, no scheduling, no renders.
-
Perfect for:
- Storing values between renders
- Tracking state that doesn't belong in UI
- Storing DOM nodes
- Flags (like skipping the first effect)
-
Quick rules-of-thumb
- Local variables (declared inside component) are reinitialized on every render — they don’t persist.
useRefpersists across renders and mutations to.currentdo not trigger re-renders.useStatepersists and does trigger re-renders when updated — use this when UI must reflect the change.
const App = () => {
const isFirstRun = useRef(true);
useEffect(() => {
if (isFirstRun.current) {
isFirstRun.current = false; // This will NOT re-render
return; // skip first effect
}
// ...
return () => {
// ...
};
});
return /*...*/;
};Because React does something special with hooks:
- Every render creates a new lexical environment.
- BUT
useRef()returns the same object instance across renders.
That "sameness" is the entire purpose of refs.
Tip
React manages refs outside the component's JS scope.
Render 1 → refVariable = { current: 0 }
Render 2 → refVariable = SAME OBJECT
Render 3 → refVariable = SAME OBJECT
...
Render N → refVariable = SAME OBJECTThe object instance does not change. Only .current changes.
const SomeComponent = ({ num }) => {
const latestNum = useRef(num);
latestNum.current = num; // keep ref up-to-date on every render
useEffect(() => {
console.log("EFFECT mounted", latestNum.current);
return () => {
// read the latest value at unmount time
console.log("EFFECT cleanup (read from ref)", latestNum.current);
};
}, []); // runs only once
};