Chapter 3: useRef
Two Uses of useRef
useRef has two main uses:
- Access a DOM element (input, div, etc.)
- Store a persistent value between renders without triggering a re-render
Access the DOM
Example: Focus on an Input
import { useRef } from 'react';
function SearchForm() {
const inputRef = useRef(null);
const handleClick = () => {
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} type="text" placeholder="Search..." />
<button onClick={handleClick}>Focus</button>
</div>
);
}
How It Works
useRef(null)creates an object{ current: null }ref={inputRef}tells React to put the DOM element ininputRef.current- After rendering,
inputRef.currentcontains the<input>element
Example: Scroll to an Element
function ScrollToSection() {
const sectionRef = useRef(null);
const scrollToSection = () => {
sectionRef.current.scrollIntoView({ behavior: 'smooth' });
};
return (
<div>
<button onClick={scrollToSection}>Go to section</button>
{/* ... lots of content ... */}
<section ref={sectionRef}>
<h2>Target Section</h2>
</section>
</div>
);
}
Example: Measure an Element
function MeasuredBox() {
const boxRef = useRef(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useEffect(() => {
if (boxRef.current) {
const { width, height } = boxRef.current.getBoundingClientRect();
setDimensions({ width, height });
}
}, []);
return (
<div>
<div ref={boxRef} style={{ padding: 20, background: '#eee' }}>
Box content
</div>
<p>Dimensions: {dimensions.width} x {dimensions.height}</p>
</div>
);
}
Store a Persistent Value
Difference from useState
| Criteria | useState | useRef |
|---|---|---|
| Persists between renders | Yes | Yes |
| Triggers a re-render | Yes | No |
| Access value | state |
ref.current |
Example: Count Renders
function RenderCounter() {
const [count, setCount] = useState(0);
const renderCount = useRef(0);
// Incremented on every render, without causing a re-render
renderCount.current += 1;
return (
<div>
<p>Count: {count}</p>
<p>Renders: {renderCount.current}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
);
}
Example: Previous Value
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
function Counter() {
const [count, setCount] = useState(0);
const previousCount = usePrevious(count);
return (
<div>
<p>Current: {count}</p>
<p>Previous: {previousCount}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
);
}
Example: Avoid useEffect Re-executions
function Timer({ onTick }) {
const onTickRef = useRef(onTick);
// Update ref without triggering a re-render
useEffect(() => {
onTickRef.current = onTick;
}, [onTick]);
useEffect(() => {
const interval = setInterval(() => {
onTickRef.current(); // Always uses the latest version
}, 1000);
return () => clearInterval(interval);
}, []); // No need for onTick in dependencies
}
Use Case with Uncontrolled Forms
Sometimes, it's simpler to read the value from the DOM directly:
function UncontrolledForm() {
const nameRef = useRef(null);
const emailRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
console.log({
name: nameRef.current.value,
email: emailRef.current.value
});
};
return (
<form onSubmit={handleSubmit}>
<input ref={nameRef} type="text" placeholder="Name" />
<input ref={emailRef} type="email" placeholder="Email" />
<button type="submit">Submit</button>
</form>
);
}
Note: controlled forms (with useState) are generally preferred as they allow real-time validation.
Warning: Do not read/write during rendering
// INCORRECT
function BadComponent() {
const ref = useRef(0);
ref.current += 1; // Modification during rendering
return <div>{ref.current}</div>;
}
// CORRECT - modify in useEffect or event handler
function GoodComponent() {
const ref = useRef(0);
useEffect(() => {
ref.current += 1; // OK in useEffect
});
const handleClick = () => {
ref.current += 1; // OK in handler
};
return <div>...</div>;
}
Exercises
Exercise 1: Auto-focus on Mount
Create a login form that automatically focuses on the email field on load.
Solution:
function LoginForm() {
const emailRef = useRef(null);
useEffect(() => {
emailRef.current.focus();
}, []);
return (
<form>
<input ref={emailRef} type="email" placeholder="Email" />
<input type="password" placeholder="Password" />
<button type="submit">Login</button>
</form>
);
}
Exercise 2: Stopwatch with Pause
Create a stopwatch with Start/Pause/Reset buttons.
Solution:
function Stopwatch() {
const [time, setTime] = useState(0);
const [isRunning, setIsRunning] = useState(false);
const intervalRef = useRef(null);
useEffect(() => {
if (isRunning) {
intervalRef.current = setInterval(() => {
setTime(t => t + 1);
}, 1000);
}
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [isRunning]);
const start = () => setIsRunning(true);
const pause = () => setIsRunning(false);
const reset = () => {
setIsRunning(false);
setTime(0);
};
return (
<div>
<p>{time} seconds</p>
<button onClick={start} disabled={isRunning}>Start</button>
<button onClick={pause} disabled={!isRunning}>Pause</button>
<button onClick={reset}>Reset</button>
</div>
);
}
Key Points to Remember
- useRef for DOM:
ref={myRef}thenmyRef.current - useRef to persist: stores a value without re-render
- Do not modify during rendering: only in useEffect or events
- Value in
.current:ref.currentand notref
<- Previous Chapter | Back to module | Next Chapter: useContext ->