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
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:
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" />
}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:
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:
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:
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:
// 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
refto a function component that does not useforwardRefFix: wrap the component with
forwardRefand attach the second argument to the DOM element you want to exposeIf the component is from a library, check its docs for a
refforwarding API or aninnerRefprop