ReactForwarding Refs

Forwarding Refs

When you attach a ref to a built-in DOM element like <input> or <button>, React sets ref.current to the DOM node automatically. But when you attach a ref to a custom component, React does not forward it to the DOM node inside — the ref is not a prop and it never reaches the inner element.

The Problem

JSX
function FancyInput(props) {
  return <input className="fancy" {...props} />
}

// Parent wants to focus the input imperatively
function Form() {
  const inputRef = useRef(null)

  useEffect(() => {
    // BAD: inputRef.current is null — FancyInput does not forward the ref
    inputRef.current?.focus()
  }, [])

  return <FancyInput ref={inputRef} placeholder="Name" />
}

// React will warn: "FancyInput: ref is not a prop. Attempting to access it
// will fail. Did you mean to use React.forwardRef()?"
The Solution — React.forwardRef

Wrap your component in React.forwardRef. The function receives props as the first argument and ref as the second, and you attach that ref to the DOM element you want to expose:

JSX
import { forwardRef } from 'react'

// forwardRef gives the component access to the ref passed by its parent
const FancyInput = forwardRef(function FancyInput(props, ref) {
  return <input ref={ref} className="fancy" {...props} />
})

// Now the parent's ref.current points to the real <input> DOM node
function Form() {
  const inputRef = useRef(null)

  useEffect(() => {
    inputRef.current?.focus()   // works!
  }, [])

  return <FancyInput ref={inputRef} placeholder="Name" />
}
Note
ref is not included in the props object. It is always the second argument to the forwardRef callback. If you try to read props.ref, you will get undefined.
A Complete Custom Input Component

Here is a production-quality text field that forwards its ref, so any parent can focus, blur, or select the inner input:

JSX
import { forwardRef } from 'react'

const TextInput = forwardRef(function TextInput(
  { label, error, className, ...rest },
  ref
) {
  return (
    <div className={['field', error && 'field--error', className].filter(Boolean).join(' ')}>
      {label && <label>{label}</label>}
      <input ref={ref} aria-invalid={!!error} {...rest} />
      {error && <span className="field__error">{error}</span>}
    </div>
  )
})

// Usage in a form:
function SignupForm() {
  const emailRef = useRef(null)

  function handleSubmit(e) {
    e.preventDefault()
    if (!emailRef.current.value.includes('@')) {
      emailRef.current.focus()   // focus the field on invalid email
      return
    }
    // submit...
  }

  return (
    <form onSubmit={handleSubmit}>
      <TextInput
        ref={emailRef}
        label="Email"
        type="email"
        placeholder="you@example.com"
      />
      <button type="submit">Sign up</button>
    </form>
  )
}
TypeScript — Typing Forwarded Refs

In TypeScript, forwardRef takes two generic parameters: the DOM element type for the ref, and the component's props type:

TSX
import { forwardRef, InputHTMLAttributes } from 'react'

interface TextInputProps extends InputHTMLAttributes<HTMLInputElement> {
  label?: string
  error?: string
}

// forwardRef<RefType, PropsType>
const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
  function TextInput({ label, error, ...rest }, ref) {
    return (
      <div>
        {label && <label>{label}</label>}
        <input ref={ref} {...rest} />
        {error && <span>{error}</span>}
      </div>
    )
  }
)

// Parent — TypeScript infers the correct ref type
function Form() {
  const inputRef = useRef<HTMLInputElement>(null)

  function focusInput() {
    inputRef.current?.focus()   // fully typed — no casting needed
  }

  return <TextInput ref={inputRef} label="Name" />
}
Setting displayName for DevTools

By default, a forwardRef component appears in React DevTools as ForwardRef. Set displayName to give it a readable name:

JSX
const TextInput = forwardRef(function TextInput(props, ref) {
  return <input ref={ref} {...props} />
})

// Appears as "TextInput" in DevTools (already handled if you name the inner function)
// But for arrow-function style:
const IconButton = forwardRef((props, ref) => (
  <button ref={ref} {...props} />
))
IconButton.displayName = 'IconButton'
Passing Refs Through Multiple Layers

Forwarding can chain across multiple wrapper components. Each layer must explicitly forward the ref:

JSX
// Layer 1 — base input
const BaseInput = forwardRef((props, ref) => (
  <input ref={ref} {...props} />
))

// Layer 2 — adds a label
const LabeledInput = forwardRef(({ label, ...rest }, ref) => (
  <div>
    <label>{label}</label>
    <BaseInput ref={ref} {...rest} />   // forward ref down to BaseInput
  </div>
))

// Layer 3 — adds validation
const ValidatedInput = forwardRef(({ validate, ...rest }, ref) => (
  <LabeledInput ref={ref} {...rest} />   // forward again
))

// Parent's ref ends up on the <input> DOM node regardless of depth
function Page() {
  const ref = useRef(null)
  return <ValidatedInput ref={ref} label="Email" validate={(v) => !!v} />
}
When You See "ref is not a prop"
  • You passed a ref to a function component that does not use forwardRef

  • Fix: wrap the component with forwardRef and attach the second argument to the DOM element you want to expose

  • If the component is from a library, check its docs for a ref forwarding API or an innerRef prop

Warning
Do not forward refs unless you deliberately want to expose a DOM node to the parent. Exposing implementation details creates tight coupling. Prefer callbacks and state for most parent-child communication.
Tip
If you need to expose more than just a DOM node — for example, specific imperative methods — combine forwardRef with useImperativeHandle. See the useImperativeHandle page for details.