Forwarding ref: The React 19 Way
What you learn in this chapter
You learn how to access DOM elements using ref. In React 19, the way you write this is simpler than before. You learn the new way as the main approach, and also cover the old way (forwardRef) so you can "read existing code".
What is ref
ref (reference) is a mechanism that holds a reference to a DOM element. You create it with useRef and pass it to an element, which lets you operate that DOM element directly.
import { useRef, useEffect } from "react"
function Example() {
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
inputRef.current?.focus() // access the DOM element directly and focus it
}, [])
return <input ref={inputRef} />
}
When ref is needed
| Use case | Description |
|---|---|
| Focus management | Focus a specific input field |
| Text selection | Select text programmatically |
| Media control | Play / pause video or audio |
| Getting size and position | Measure an element's size or coordinates |
| Third-party integration | Combine with libraries that need DOM manipulation |
For normal styling or event handling, ref is not needed. You use it when you "want to touch the DOM itself".
React 19: ref is a normal prop
In React 19, a function component can receive ref as a normal prop.
// ✅ React 19: receive ref as a prop
function MyButton({ ref, ...props }: React.ComponentProps<"button">) {
return <button ref={ref} {...props} />
}
function App() {
const buttonRef = useRef<HTMLButtonElement>(null)
return <MyButton ref={buttonRef}>Click</MyButton> // works as-is
}
You just receive ref like { ref, ...props } and pass it to the inner <button>. No special mechanism is needed.
<MyButton ref={buttonRef}>
│
↓ in React 19, ref is part of props
function MyButton({ ref, ...props }) {
return <button ref={ref} /> ← pass it through as-is
}
ref in Button
Recall the Button from the final-form chapter. Did you notice that the word ref never appeared?
function Button({ className, variant, size, asChild = false, ...props }: ...) {
const Comp = asChild ? Slot.Root : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props} // ← ref is included here and passed through
/>
)
}
In React 19, ref is also a normal prop, so it is included in ...props. Just spreading <Comp {...props} /> passes ref along to the <button> (or Slot) too. That is why the Button code needs no special handling for ref.
Let's look concretely at what goes into ...props.
// the calling side
<Button ref={buttonRef} onClick={handleClick} disabled type="submit">
Submit
</Button>
At this point, the destructuring inside Button looks like this.
// the 4 named ones are extracted individually
// className = undefined / variant = undefined / size = undefined / asChild = false
// everything else stays in ...props
props = {
ref: buttonRef, // in React 19, ref is also a normal prop
onClick: handleClick,
disabled: true,
type: "submit",
children: "Submit", // ← the tag's content is also included as children
}
className / variant / size / asChild are extracted first, so they are not included in ...props (only className is passed separately via cn(...)). On the other hand, the key point is that not only ref but also children (the tag's content) goes into ...props. <Comp {...props} /> passes this entire remainder to the <button> (or Slot) all at once.
With this, consumers can pass ref normally.
const buttonRef = useRef<HTMLButtonElement>(null)
<Button ref={buttonRef}>Submit</Button>
Hands-on examples with ref
Example 1: auto-focus on mount
import { useRef, useEffect } from "react"
import { Button } from "@/components/ui/button"
function AutoFocusButton() {
const buttonRef = useRef<HTMLButtonElement>(null)
useEffect(() => {
buttonRef.current?.focus()
}, [])
return <Button ref={buttonRef}>This is focused automatically</Button>
}
Example 2: clicking from another button
function RemoteClickButton() {
const buttonRef = useRef<HTMLButtonElement>(null)
return (
<>
<Button ref={buttonRef} onClick={() => alert("Clicked")}>
The main button
</Button>
<button onClick={() => buttonRef.current?.click()}>
Click the button above remotely
</button>
</>
)
}
Example 3: measuring an element's size
import { useRef, useEffect, useState } from "react"
import { Button } from "@/components/ui/button"
function MeasureButton() {
const buttonRef = useRef<HTMLButtonElement>(null)
const [size, setSize] = useState({ width: 0, height: 0 })
useEffect(() => {
if (buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect()
setSize({ width: rect.width, height: rect.height })
}
}, [])
return (
<>
<Button ref={buttonRef}>Measure me</Button>
<p>Width: {size.width}px / Height: {size.height}px</p>
</>
)
}
Combining with asChild
When you use asChild, you write ref on the substituted element (the child element).
<Button asChild>
<a href="/profile" ref={linkRef}>Profile</a>
</Button>
// ref points to the <a> element
The Button's ref type stays HTMLButtonElement, so write the ref of the element substituted via asChild on the child element. The mechanism of Slot is covered in the next chapter.
Supplement: the old way, "forwardRef"
In existing code and libraries, you often see a style called forwardRef. This was the way that was needed in React 18 and earlier.
Back then, function components could not receive ref as a prop. So you wrapped them with forwardRef to receive ref.
import { forwardRef } from "react"
// the React 18 and earlier way
const MyButton = forwardRef<HTMLButtonElement, Props>((props, ref) => {
return <button ref={ref} {...props} />
})
MyButton.displayName = "MyButton" // for the DevTools display
Until recently, shadcn/ui's Button was also written with this forwardRef.
In React 19, you can receive ref as a normal prop, and forwardRef became unnecessary. forwardRef will keep working for now, but it is scheduled to be deprecated and eventually removed (as of React 19, no warning is shown yet). The React team provides a codemod that rewrites it automatically, so you can migrate old code mechanically (React 19 official blog).
In short, for new code, do not use forwardRef; receive ref as a prop is the correct approach. forwardRef is enough to remember as "knowledge for reading older code".
ref and styling are separate concerns
ref (DOM access) and cva / cn (styling) are independent of each other.
3 independent concerns
├─ cva … variants
├─ cn … conflict resolution
└─ ref … DOM access
↓ combine into a single
<button> or <Slot>
Each has its own role, and together they form one Button.
Chapter summary
- ref is a reference to a DOM element (used for focus, measurement, media control, and so on)
- In React 19, you can receive
refas a normal prop (noforwardRef) - Button passes
refthrough transparently with...props, so no special handling is needed forwardRefis the old way. It works for now but will be deprecated in the future. Do not use it in new code
In the next chapter, you learn the mechanism of Slot, which makes asChild possible.