useRef Hook
The useRef hook creates a mutable reference that persists across renders without causing re-renders when updated.
Basic Syntax
jsx
1import { useRef } from 'react'
2
3function Component() {
4 const ref = useRef(initialValue)
5
6 // Access value with .current
7 console.log(ref.current)
8
9 // Update without re-render
10 ref.current = newValue
11}DOM References
The most common use - accessing DOM elements:
jsx
1function TextInput() {
2 const inputRef = useRef(null)
3
4 const focusInput = () => {
5 inputRef.current.focus()
6 }
7
8 const selectAll = () => {
9 inputRef.current.select()
10 }
11
12 return (
13 <div>
14 <input ref={inputRef} type="text" />
15 <button onClick={focusInput}>Focus</button>
16 <button onClick={selectAll}>Select All</button>
17 </div>
18 )
19}Auto-Focus on Mount
jsx
1function SearchBox() {
2 const inputRef = useRef(null)
3
4 useEffect(() => {
5 inputRef.current?.focus()
6 }, [])
7
8 return <input ref={inputRef} placeholder="Search..." />
9}Scroll to Element
jsx
1function LongPage() {
2 const topRef = useRef(null)
3 const bottomRef = useRef(null)
4
5 const scrollToTop = () => {
6 topRef.current?.scrollIntoView({ behavior: 'smooth' })
7 }
8
9 const scrollToBottom = () => {
10 bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
11 }
12
13 return (
14 <div>
15 <div ref={topRef}>Top of page</div>
16
17 {/* Long content */}
18
19 <div ref={bottomRef}>Bottom of page</div>
20
21 <button onClick={scrollToTop}>Go to top</button>
22 <button onClick={scrollToBottom}>Go to bottom</button>
23 </div>
24 )
25}Video/Audio Control
jsx
1function VideoPlayer() {
2 const videoRef = useRef(null)
3 const [isPlaying, setIsPlaying] = useState(false)
4
5 const togglePlay = () => {
6 if (isPlaying) {
7 videoRef.current.pause()
8 } else {
9 videoRef.current.play()
10 }
11 setIsPlaying(!isPlaying)
12 }
13
14 const skip = (seconds) => {
15 videoRef.current.currentTime += seconds
16 }
17
18 return (
19 <div>
20 <video ref={videoRef} src="/video.mp4" />
21 <button onClick={togglePlay}>
22 {isPlaying ? 'Pause' : 'Play'}
23 </button>
24 <button onClick={() => skip(-10)}>-10s</button>
25 <button onClick={() => skip(10)}>+10s</button>
26 </div>
27 )
28}Storing Previous Values
jsx
1function Counter() {
2 const [count, setCount] = useState(0)
3 const prevCountRef = useRef(0)
4
5 useEffect(() => {
6 prevCountRef.current = count
7 }, [count])
8
9 return (
10 <div>
11 <p>Current: {count}</p>
12 <p>Previous: {prevCountRef.current}</p>
13 <button onClick={() => setCount(c => c + 1)}>+</button>
14 </div>
15 )
16}
17
18// Custom hook for previous value
19function usePrevious(value) {
20 const ref = useRef()
21
22 useEffect(() => {
23 ref.current = value
24 }, [value])
25
26 return ref.current
27}
28
29// Usage
30function Component() {
31 const [count, setCount] = useState(0)
32 const prevCount = usePrevious(count)
33}Storing Instance Variables
Unlike state, updating ref doesn't trigger re-render:
jsx
1function Stopwatch() {
2 const [time, setTime] = useState(0)
3 const [isRunning, setIsRunning] = useState(false)
4 const intervalRef = useRef(null)
5
6 const start = () => {
7 if (!isRunning) {
8 setIsRunning(true)
9 intervalRef.current = setInterval(() => {
10 setTime(t => t + 1)
11 }, 1000)
12 }
13 }
14
15 const stop = () => {
16 if (isRunning) {
17 setIsRunning(false)
18 clearInterval(intervalRef.current)
19 }
20 }
21
22 const reset = () => {
23 stop()
24 setTime(0)
25 }
26
27 useEffect(() => {
28 return () => clearInterval(intervalRef.current)
29 }, [])
30
31 return (
32 <div>
33 <p>{time}s</p>
34 <button onClick={start}>Start</button>
35 <button onClick={stop}>Stop</button>
36 <button onClick={reset}>Reset</button>
37 </div>
38 )
39}Tracking Mount State
jsx
1function useIsMounted() {
2 const isMounted = useRef(true)
3
4 useEffect(() => {
5 return () => {
6 isMounted.current = false
7 }
8 }, [])
9
10 return isMounted
11}
12
13// Prevent state updates on unmounted component
14function AsyncComponent() {
15 const [data, setData] = useState(null)
16 const isMounted = useIsMounted()
17
18 useEffect(() => {
19 fetchData().then(result => {
20 if (isMounted.current) {
21 setData(result)
22 }
23 })
24 }, [])
25}Measuring Elements
jsx
1function MeasuredBox() {
2 const boxRef = useRef(null)
3 const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
4
5 useEffect(() => {
6 if (boxRef.current) {
7 const { width, height } = boxRef.current.getBoundingClientRect()
8 setDimensions({ width, height })
9 }
10 }, [])
11
12 return (
13 <div ref={boxRef} style={{ width: '50%', padding: 20 }}>
14 <p>Width: {dimensions.width}px</p>
15 <p>Height: {dimensions.height}px</p>
16 </div>
17 )
18}Callback Refs
For more control over when refs are set:
jsx
1function CallbackRefExample() {
2 const [height, setHeight] = useState(0)
3
4 const measuredRef = useCallback(node => {
5 if (node !== null) {
6 setHeight(node.getBoundingClientRect().height)
7 }
8 }, [])
9
10 return (
11 <div ref={measuredRef}>
12 <p>Height: {height}px</p>
13 </div>
14 )
15}Forwarding Refs
Pass refs to child components:
jsx
1import { forwardRef, useRef } from 'react'
2
3// Child component accepts forwarded ref
4const FancyInput = forwardRef((props, ref) => {
5 return <input ref={ref} className="fancy" {...props} />
6})
7
8// Parent can now access child's DOM element
9function Parent() {
10 const inputRef = useRef(null)
11
12 const focus = () => inputRef.current?.focus()
13
14 return (
15 <div>
16 <FancyInput ref={inputRef} placeholder="Type here" />
17 <button onClick={focus}>Focus Input</button>
18 </div>
19 )
20}useRef vs useState
| useRef | useState |
|---|---|
| Doesn't trigger re-render | Triggers re-render |
| Value persists across renders | Value persists across renders |
| Mutable (.current) | Immutable (use setter) |
| Sync updates | Batched updates |
| Good for DOM refs, timers, previous values | Good for UI state |
useRef is essential for working with DOM elements and mutable values!
