Skip to main content

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 caseDescription
Focus managementFocus a specific input field
Text selectionSelect text programmatically
Media controlPlay / pause video or audio
Getting size and positionMeasure an element's size or coordinates
Third-party integrationCombine 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. The word ref never appeared in that code.

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.

// 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, 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.

info

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).

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>

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 ref as a normal prop (no forwardRef)
  • Button passes ref through transparently with ...props, so no special handling is needed
  • forwardRef is 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.