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

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

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