useRef and DOM manipulation
In this chapter, you learn about the useRef hook, used for direct DOM access and holding values, and related APIs.
What you'll learn in this chapter
- The two uses of useRef (DOM references and holding values)
- The difference from useState
- Ref forwarding (React 19's ref props / forwardRef in React 18 and earlier) and useImperativeHandle
- When to use ref and when to avoid it
This chapter uses the project created in Chapter 3. Start the development server with npm run dev and study while writing code.
What is useRef
useRef is a hook that holds a value across renders and does not re-render the component when you change it. It has two main uses.
- References to DOM elements: such as focusing an input element
- Holding values: managing values you don't want to trigger a re-render, such as holding a timer ID
The "uncontrolled components" touched on in Chapter 10 use this useRef to get values directly from the DOM. For managing form values, useState (controlled components) is common, but things like file input require useRef.
Basic usage
References to DOM elements
import { useRef } from 'react'
function TextInput() {
// Create a ref object with useRef<type>(initial value)
// <HTMLInputElement> is the type of the DOM element to reference
// null is the initial value (because the DOM element doesn't exist yet)
const inputRef = useRef<HTMLInputElement>(null)
const handleClick = () => {
// Access the DOM element with ref.current
// ?. is optional chaining (skips if null)
inputRef.current?.focus()
}
return (
<div>
{/* Associate the DOM element with the ref object via the ref attribute */}
<input ref={inputRef} type="text" />
<button onClick={handleClick}>Focus</button>
</div>
)
}
The structure of useRef
const ref = useRef(initialValue)
// The value is stored in ref.current
console.log(ref.current) // initialValue
The difference between useRef and useState
| Feature | useRef | useState |
|---|---|---|
| Re-render on value change | No | Yes |
| Accessing the value | ref.current | Directly |
| Main uses | DOM references, holding the previous value | State reflected in the UI |
function Comparison() {
// useState: changing the value triggers a re-render
const [stateCount, setStateCount] = useState(0)
// useRef: changing the value does not trigger a re-render
const refCount = useRef(0)
const handleStateClick = () => {
setStateCount(stateCount + 1) // triggers a re-render → the screen updates
console.log('state:', stateCount + 1)
}
const handleRefClick = () => {
refCount.current += 1 // doesn't trigger a re-render → the screen doesn't update
console.log('ref:', refCount.current)
}
return (
<div>
<p>State: {stateCount}</p>
{/* Even if refCount.current changes, without a re-render it isn't reflected on screen */}
<p>Ref: {refCount.current}</p>
<button onClick={handleStateClick}>State +1</button>
<button onClick={handleRefClick}>Ref +1</button>
</div>
)
}
Practical examples
Focusing an input form
function LoginForm() {
const emailRef = useRef<HTMLInputElement>(null)
const passwordRef = useRef<HTMLInputElement>(null)
// Focus the email field on mount
useEffect(() => {
emailRef.current?.focus()
}, [])
const handleEmailKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
passwordRef.current?.focus()
}
}
return (
<form>
<input
ref={emailRef}
type="email"
placeholder="Email"
onKeyDown={handleEmailKeyDown}
/>
<input
ref={passwordRef}
type="password"
placeholder="Password"
/>
<button type="submit">Log in</button>
</form>
)
}
Let's try it: autofocus
Create a login form where the input field is automatically focused when the page loads.
Hint
- Create a ref with
useRef<HTMLInputElement>(null) - Call
focus()on mount (an empty dependency array) inuseEffect - Moving to the password field with the Enter key is convenient
Answer and explanation
import { useRef, useEffect } from 'react'
import './App.css'
function LoginForm() {
const emailRef = useRef<HTMLInputElement>(null)
const passwordRef = useRef<HTMLInputElement>(null)
useEffect(() => {
emailRef.current?.focus()
}, [])
const handleEmailKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault()
passwordRef.current?.focus()
}
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
console.log('Login process')
}
return (
<form onSubmit={handleSubmit}>
<div>
<input
ref={emailRef}
type="email"
placeholder="Email"
onKeyDown={handleEmailKeyDown}
/>
</div>
<div>
<input
ref={passwordRef}
type="password"
placeholder="Password"
/>
</div>
<button type="submit">Log in</button>
</form>
)
}
export default function App() {
return <LoginForm />
}
By making useEffect's dependency array empty, focus() runs once on mount. We also added a feature to move to the password field with the Enter key.
Video player
function VideoPlayer() {
const videoRef = useRef<HTMLVideoElement>(null)
const [isPlaying, setIsPlaying] = useState(false)
const handlePlayPause = () => {
if (videoRef.current) {
if (isPlaying) {
videoRef.current.pause()
} else {
videoRef.current.play()
}
setIsPlaying(!isPlaying)
}
}
return (
<div>
<video ref={videoRef} src="/video.mp4" />
<button onClick={handlePlayPause}>
{isPlaying ? 'Pause' : 'Play'}
</button>
</div>
)
}
Controlling the scroll position
function ScrollToBottom() {
const bottomRef = useRef<HTMLDivElement>(null)
const [messages, setMessages] = useState<string[]>([])
const addMessage = () => {
setMessages(prev => [...prev, `Message ${prev.length + 1}`])
}
// Scroll to the bottom when a message is added
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [messages])
return (
<div>
<div style={{ height: '200px', overflow: 'auto' }}>
{messages.map((msg, i) => (
<p key={i}>{msg}</p>
))}
<div ref={bottomRef} />
</div>
<button onClick={addMessage}>Add message</button>
</div>
)
}
Let's try it: scrolling a chat UI
Create a chat UI that automatically scrolls to the bottom each time a message is added.
Hint
- Place an empty
divat the bottom of the message list - Set a ref on that element
- Call
scrollIntoView()when a message is added - Use
behavior: 'smooth'to scroll smoothly
Answer and explanation
import { useState, useRef, useEffect } from 'react'
import './App.css'
function ChatUI() {
const [messages, setMessages] = useState<string[]>([])
const [input, setInput] = useState('')
const bottomRef = useRef<HTMLDivElement>(null)
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [messages])
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInput(e.target.value)
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (input.trim()) {
setMessages(prev => [...prev, input])
setInput('')
}
}
return (
<div>
<div style={{ height: '300px', overflow: 'auto', border: '1px solid #ccc' }}>
{messages.map((msg, index) => (
<div key={index} style={{ padding: '8px', borderBottom: '1px solid #eee' }}>
{msg}
</div>
))}
<div ref={bottomRef} />
</div>
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={handleInputChange}
placeholder="Enter a message"
/>
<button type="submit">Send</button>
</form>
</div>
)
}
export default function App() {
return <ChatUI />
}
We place an empty div at the end of the message list and scroll with scrollIntoView(). behavior: 'smooth' produces a smooth scroll animation.
Holding the previous value
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined)
useEffect(() => {
ref.current = value
}, [value])
return ref.current
}
// Usage
function Counter() {
const [count, setCount] = useState(0)
const prevCount = usePrevious(count)
const handleIncrement = () => {
setCount(prev => prev + 1)
}
return (
<div>
<p>Current: {count}, Previous: {prevCount}</p>
<button onClick={handleIncrement}>+1</button>
</div>
)
}
Let's try it: display the previous value
Create a counter that displays both the current value and the previous value.
Hint
- Create a
usePreviouscustom hook - Combine
useRefanduseEffectto hold the previous value - Leverage the fact that
useEffectruns after rendering
Answer and explanation
import { useState, useRef, useEffect } from 'react'
import './App.css'
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined)
useEffect(() => {
ref.current = value
}, [value])
return ref.current
}
function CounterWithPrevious() {
const [count, setCount] = useState(0)
const prevCount = usePrevious(count)
const handleIncrement = () => {
setCount(prev => prev + 1)
}
const handleDecrement = () => {
setCount(prev => prev - 1)
}
const handleReset = () => {
setCount(0)
}
return (
<div>
<p>Current value: {count}</p>
<p>Previous value: {prevCount ?? '(none)'}</p>
<button onClick={handleIncrement}>+1</button>
<button onClick={handleDecrement}>-1</button>
<button onClick={handleReset}>Reset</button>
</div>
)
}
export default function App() {
return <CounterWithPrevious />
}
The usePrevious custom hook leverages the fact that useEffect runs after rendering. On the first render, ref.current is undefined, and after useEffect runs, the current value is saved. On the next render, the previously saved value is returned.
Holding a timer ID
function Timer() {
const [seconds, setSeconds] = useState(0)
const [isRunning, setIsRunning] = useState(false)
// Hold the timer ID with useRef (hold the value without triggering a re-render)
// number | null is the type of setInterval's return value
const intervalRef = useRef<number | null>(null)
const start = () => {
if (!isRunning) {
setIsRunning(true)
// Save setInterval's ID in ref.current
intervalRef.current = window.setInterval(() => {
setSeconds(prev => prev + 1)
}, 1000)
}
}
const stop = () => {
if (intervalRef.current) {
// Call clearInterval with the saved ID
clearInterval(intervalRef.current)
intervalRef.current = null
}
setIsRunning(false)
}
const reset = () => {
stop()
setSeconds(0)
}
// Cleanup: stop the timer when the component unmounts
useEffect(() => {
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
}
}
}, [])
return (
<div>
<p>{seconds}s</p>
<button onClick={start} disabled={isRunning}>Start</button>
<button onClick={stop} disabled={!isRunning}>Stop</button>
<button onClick={reset}>Reset</button>
</div>
)
}
Forwarding a ref to a child component
This is the pattern for when you want to access a child component's DOM element from the parent. From React 19, it has become much simpler.
React 19+: receive ref as a prop (recommended)
From React 19, a function component can receive ref just like a regular Prop. You don't need to wrap it with forwardRef.
import { useRef } from 'react'
type CustomInputProps = {
placeholder?: string
ref?: React.Ref<HTMLInputElement>
}
// Just receive ref as a regular prop
function CustomInput({ ref, ...props }: CustomInputProps) {
return <input ref={ref} className="custom-input" {...props} />
}
// Use it in the parent component
function Form() {
const inputRef = useRef<HTMLInputElement>(null)
const handleClick = () => {
inputRef.current?.focus()
}
return (
<div>
<CustomInput ref={inputRef} placeholder="Enter text" />
<button onClick={handleClick}>Focus</button>
</div>
)
}
forwardRef (React 18 and earlier / for compatibility)
Since you'll encounter forwardRef in React 18 and below or in existing code, we note it here for reference.
import { forwardRef, useRef } from 'react'
// forwardRef: forwards the ref received from the parent to the child's DOM element
// forwardRef<the DOM element type, the Props type>(the component function)
const CustomInput = forwardRef<HTMLInputElement, { placeholder?: string }>(
// 1st argument: props, 2nd argument: the ref forwarded from the parent
(props, ref) => {
return (
<input
ref={ref} // associate the ref passed from the parent with the input element
className="custom-input"
{...props}
/>
)
}
)
When using forwardRef, setting a displayName on the component makes debugging in React DevTools easier.
From React 19, forwardRef is not deprecated, but it has become unnecessary. In new code, use the simple prop-receiving form, and there's no need to rewrite existing forwardRef. Gradual migration is enough. Note that the React team has announced plans to deprecate and remove forwardRef in a future version.
A practical example: a custom button
type ButtonProps = {
children: React.ReactNode
variant?: 'primary' | 'secondary'
ref?: React.Ref<HTMLButtonElement>
} & React.ButtonHTMLAttributes<HTMLButtonElement>
function Button({ children, variant = 'primary', className, ref, ...props }: ButtonProps) {
const baseClasses = 'px-4 py-2 rounded font-medium'
const variantClasses = {
primary: 'bg-blue-500 text-white hover:bg-blue-600',
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300'
}
return (
<button
ref={ref}
className={`${baseClasses} ${variantClasses[variant]} ${className ?? ''}`}
{...props}
>
{children}
</button>
)
}
ref callback and cleanup function (React 19+)
The ref attribute can take not only a ref object but also a function (a ref callback). It's called with the element as an argument at the moment the element is added to the DOM. Furthermore, from React 19, a ref callback can return a cleanup function.
<div
ref={node => {
// Called when the element is added to the DOM
console.log('attached:', node)
// Returning a cleanup function calls it when the element is removed from the DOM (on detach) (React 19 and later)
return () => {
console.log('detached')
}
}}
/>
useImperativeHandle
Using useImperativeHandle, you can customize the value of the ref exposed to the parent. This lets you expose only specific methods rather than the entire DOM element.
import { useRef, useImperativeHandle } from 'react'
// Type definition of the exposed methods (not the DOM element itself, but only specific methods)
type CustomInputHandle = {
focus: () => void
clear: () => void
getValue: () => string
}
// Receive ref as a prop of type CustomInputHandle
function CustomInput({ label, ref }: { label: string; ref?: React.Ref<CustomInputHandle> }) {
// Hold an internal reference to the DOM element
const inputRef = useRef<HTMLInputElement>(null)
// useImperativeHandle: customize the methods exposed to the parent
// 1st argument: the ref received from the parent
// 2nd argument: a function that returns the object to expose
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current?.focus()
},
clear: () => {
if (inputRef.current) {
inputRef.current.value = ''
}
},
getValue: () => {
return inputRef.current?.value ?? ''
}
}))
return (
<div>
<label>{label}</label>
<input ref={inputRef} type="text" />
</div>
)
}
// Parent component
function Form() {
// Create a ref of type CustomInputHandle (custom methods, not a DOM element)
const inputRef = useRef<CustomInputHandle>(null)
const handleFocus = () => {
// Call the method exposed by the child component
inputRef.current?.focus()
}
const handleSubmit = () => {
const value = inputRef.current?.getValue()
console.log('Input value:', value)
inputRef.current?.clear()
}
return (
<div>
<CustomInput ref={inputRef} label="Name" />
<button onClick={handleFocus}>Focus</button>
<button onClick={handleSubmit}>Send</button>
</div>
)
}
Use cases
useImperativeHandle is useful in the following cases.
- When you don't want to expose the entire DOM: From a security and safety standpoint, allow only specific operations
- An abstracted API: Hide the details of DOM manipulation and provide easy-to-understand methods
- Complex components: Provide a unified interface for a component that has multiple DOM elements
Limit the use of useImperativeHandle to when it's necessary. In most cases, Props and callbacks are enough. Imperative code via ref departs from React's declarative paradigm, so avoid overusing it.
When to use ref
Appropriate use cases
- Controlling focus, text selection, and media playback
- Triggering animations
- Integrating with third-party DOM libraries
- Holding values that don't affect rendering, such as the previous value or a timer ID
Cases to avoid
- Problems that can be solved declaratively (when State is enough)
- Holding data needed for rendering (you should use State)
- When you frequently read and write ref.current
// NG: trying to control the UI with ref
function BadExample() {
const divRef = useRef<HTMLDivElement>(null)
const handleClick = () => {
if (divRef.current) {
divRef.current.style.display = 'none' // imperative
}
}
return <div ref={divRef}>...</div>
}
// OK: control declaratively with State
function GoodExample() {
const [isVisible, setIsVisible] = useState(true)
return isVisible ? <div>...</div> : null
}
Summary
useRefholds a value without triggering a re-render- Use it to get a reference to a DOM element
- Use it to hold values that don't affect the UI, such as a timer ID or the previous value
- Receive
refas a prop to expose a child component's DOM element to the parent (forwardRefis the React 18-and-earlier method) - Customize the exposed methods with
useImperativeHandle - Limit the use of ref to when it's necessary, and prefer a declarative approach whenever possible
In the next chapter, you learn about global state sharing with useContext.