useRef is a hook in React that returns a mutable ref object which persists across component renders. The main property of this object is current
, which can store any value.
Key features of useRef:
.current
doesn’t trigger a re-renderMain applications:
Example of accessing a DOM element:
function TextInputWithFocusButton() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
return (
<>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus the input</button>
</>
);
}
The useRef
hook is one of React’s built-in hooks that provides a way to create a mutable value that doesn’t cause a re-render when updated. This makes it ideal for certain scenarios where useState
would be overkill or inappropriate. 🔍
useRef
returns a simple JavaScript object with a single property called current
:
// Creating a ref with initial value null
const myRef = useRef(null);
console.log(myRef); // { current: null }
// You can set an initial value
const countRef = useRef(0);
console.log(countRef); // { current: 0 }
// Changing the value doesn't cause a re-render
countRef.current = countRef.current + 1;
Unlike state, changing current
happens synchronously and doesn’t cause the component to re-render.
The most common use of useRef is to get direct access to DOM nodes:
function AutoFocusInput() {
const inputRef = useRef(null);
// After the component mounts, focus the input
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} />;
}
function VideoPlayer({ src }) {
const videoRef = useRef(null);
const handlePlay = () => {
videoRef.current.play();
};
const handlePause = () => {
videoRef.current.pause();
};
return (
<div>
<video ref={videoRef} src={src} />
<button onClick={handlePlay}>Play</button>
<button onClick={handlePause}>Pause</button>
</div>
);
}
function MeasureExample() {
const divRef = useRef(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useEffect(() => {
if (divRef.current) {
setDimensions({
width: divRef.current.offsetWidth,
height: divRef.current.offsetHeight
});
}
}, []);
return (
<>
<div ref={divRef} style={{ width: '100%', height: '100px', border: '1px solid black' }}>
Element to measure
</div>
<p>Width: {dimensions.width}px, Height: {dimensions.height}px</p>
</>
);
}
Tracking previous state values that aren’t directly available:
function CounterWithPrevious() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
// Save current count value after render
prevCountRef.current = count;
});
const prevCount = prevCountRef.current;
return (
<div>
<h1>Now: {count}, before: {prevCount !== undefined ? prevCount : 'No previous value'}</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
When you need to store a value that doesn’t affect the UI:
function IntervalExample() {
const [count, setCount] = useState(0);
const intervalIdRef = useRef(null);
const startCounter = () => {
if (intervalIdRef.current !== null) return;
intervalIdRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
};
const stopCounter = () => {
clearInterval(intervalIdRef.current);
intervalIdRef.current = null;
};
// Cleanup on unmount
useEffect(() => {
return () => {
if (intervalIdRef.current !== null) {
clearInterval(intervalIdRef.current);
}
};
}, []);
return (
<div>
<h1>Counter: {count}</h1>
<button onClick={startCounter}>Start</button>
<button onClick={stopCounter}>Stop</button>
</div>
);
}
Storing computation results that shouldn’t cause re-renders:
function ExpensiveComponent({ data }) {
const cachedDataRef = useRef(null);
if (cachedDataRef.current === null || cachedDataRef.current.originalData !== data) {
// Perform expensive calculations only when data changes
const processedResult = expensiveCalculation(data);
cachedDataRef.current = {
originalData: data,
processedResult
};
}
// Use the cached result
return <div>{cachedDataRef.current.processedResult}</div>;
}
Example of a hook to track if a component is mounted:
function useIsMounted() {
const isMountedRef = useRef(false);
useEffect(() => {
isMountedRef.current = true;
return () => {
isMountedRef.current = false;
};
}, []);
return isMountedRef;
}
// Usage
function AsyncComponent() {
const [data, setData] = useState(null);
const isMountedRef = useIsMounted();
useEffect(() => {
fetchData().then(result => {
// Check if component is still mounted before updating state
if (isMountedRef.current) {
setData(result);
}
});
}, []);
return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}
// With useState - re-renders on every change
function CounterWithState() {
const [count, setCount] = useState(0);
// Re-renders the component
const increment = () => setCount(count + 1);
console.log("Rendering component with useState");
return (
<div>
<p>Counter (useState): {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
// With useRef - no re-renders
function CounterWithRef() {
const countRef = useRef(0);
const [, forceUpdate] = useState({});
// Doesn't trigger a re-render
const increment = () => {
countRef.current += 1;
};
// To display the current value we need to force a re-render
const incrementAndUpdate = () => {
increment();
forceUpdate({});
};
console.log("Rendering component with useRef");
return (
<div>
<p>Counter (useRef): {countRef.current}</p>
<button onClick={increment}>Increment (no UI update)</button>
<button onClick={incrementAndUpdate}>Increment and update UI</button>
</div>
);
}
function CompareRefs() {
// Created anew on each render
const createRefExample = React.createRef();
// Persists between renders
const useRefExample = useRef();
// Demonstrating differences
const [, forceRender] = useState({});
useEffect(() => {
console.log("After mounting:");
console.log("createRef current value:", createRefExample.current);
console.log("useRef current value:", useRefExample.current);
// Set values
createRefExample.current = "createRef value";
useRefExample.current = "useRef value";
console.log("After setting:");
console.log("createRef value:", createRefExample.current);
console.log("useRef value:", useRefExample.current);
}, []);
const handleClick = () => {
// Re-render the component
forceRender({});
// After re-render
console.log("After re-render:");
console.log("createRef value:", createRefExample.current); // null
console.log("useRef value:", useRefExample.current); // "useRef value"
};
return <button onClick={handleClick}>Re-render</button>;
}
// ❌ Incorrect: useEffect doesn't track ref.current changes
function WrongWayToWatchRef() {
const countRef = useRef(0);
useEffect(() => {
console.log("Value changed:", countRef.current);
}, [countRef.current]); // This won't work as expected
return (
<button onClick={() => { countRef.current += 1; }}>
Increment (won't trigger useEffect)
</button>
);
}
// ✅ Correct: use state for values you want to track
function CorrectWayToWatchChanges() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("Value changed:", count);
}, [count]);
return (
<button onClick={() => setCount(count + 1)}>
Increment (will trigger useEffect)
</button>
);
}
// ❌ Incorrect: creating ref inside a condition
function ConditionalRef({ shouldRender }) {
let inputRef;
if (shouldRender) {
inputRef = useRef(null);
}
// This will cause errors when shouldRender changes
return shouldRender ? <input ref={inputRef} /> : null;
}
// ✅ Correct: always create refs at the top level
function CorrectConditionalRendering({ shouldRender }) {
const inputRef = useRef(null);
return shouldRender ? <input ref={inputRef} /> : null;
}
// ❌ Incorrect: accessing ref before it's assigned
function TooEarlyAccess() {
const inputRef = useRef(null);
// This code runs before the ref is assigned
console.log(inputRef.current.value); // Error: Cannot read property 'value' of null
return <input ref={inputRef} defaultValue="Initial value" />;
}
// ✅ Correct: use useEffect to access after render
function CorrectAccess() {
const inputRef = useRef(null);
useEffect(() => {
// This code runs after the DOM is ready
console.log(inputRef.current.value);
}, []);
return <input ref={inputRef} defaultValue="Initial value" />;
}
✅ useRef in React is used for:
✅ Key features:
.current
doesn’t trigger a re-renderuseState
by not causing automatic re-renderscreateRef
by preserving value between rendersUnderstanding useRef and applying it correctly is an important part of optimizing performance and interacting with the DOM in React applications. 🚀
Want more articles to prepare for interviews? Subscribe to EasyAdvice, bookmark the site, and improve yourself every day 💪