Function components are plain functions — they have no backing instance. When React renders a class component it creates an object you can point a ref at. Function components produce nothing but JSX, so there is no object to attach the ref to by default.
function Input({ value, onChange }) {
return <input value={value} onChange={onChange} />
}
function Parent() {
const ref = useRef(null)
// ❌ ref will be null; React logs a warning
return <Input ref={ref} value="" onChange={() => {}} />
}
To let a parent reach inside, you must either wrap with React.forwardRef (React
≤ 18) or accept ref as an explicit prop (React 19+).
Rule of thumb: "No instance" means "no implicit ref" — you must opt in explicitly.
React.forwardRef wraps a render function and injects the parent-supplied ref as
a second argument alongside props. It returns a component that React treats
as a normal component but also honours the ref.
import { forwardRef } from 'react'
// forwardRef(renderFn) — renderFn receives (props, ref)
const FancyInput = forwardRef(function FancyInput(props, ref) {
return (
<input
ref={ref} // wire the forwarded ref to the DOM node
className="fancy"
{...props}
/>
)
})
// Parent
function Form() {
const inputRef = useRef(null)
return (
<>
<FancyInput ref={inputRef} placeholder="Type here" />
<button onClick={() => inputRef.current.focus()}>Focus</button>
</>
)
}
Without forwardRef, ref would not appear in the render function's argument list
at all — it is filtered out of props by React's reconciler.
Rule of thumb: forwardRef = "please pass the ref through to me as arg #2."
useImperativeHandle(ref, createHandle, deps?) replaces what the parent sees
when it reads ref.current. Instead of the raw DOM node you expose a custom object
with only the methods you choose to surface. This is the controlled-API boundary
between child and parent.
const VideoPlayer = forwardRef(function VideoPlayer({ src }, ref) {
const videoRef = useRef(null)
useImperativeHandle(ref, () => ({
// expose a curated imperative API — not the raw <video> element
play() { videoRef.current.play() },
pause() { videoRef.current.pause() },
seek(t) { videoRef.current.currentTime = t },
}))
return <video ref={videoRef} src={src} />
})
function Page() {
const player = useRef(null)
return (
<>
<VideoPlayer ref={player} src="/movie.mp4" />
<button onClick={() => player.current.play()}>Play</button>
</>
)
}
The parent can call play, pause, and seek but cannot touch the raw <video>
node — accidental DOM manipulation is prevented.
Rule of thumb: useImperativeHandle = "I'll expose only what I designed as my public API."
The standard pattern: keep the real DOM ref private inside the component; expose
only the three operations via useImperativeHandle.
import { forwardRef, useImperativeHandle, useRef } from 'react'
const SmartInput = forwardRef(function SmartInput(props, ref) {
const inputRef = useRef(null) // private — not leaked to parent
useImperativeHandle(ref, () => ({
focus() { inputRef.current.focus() },
blur() { inputRef.current.blur() },
clear() { inputRef.current.value = '' },
}), []) // no deps — handle never changes
return <input ref={inputRef} {...props} />
})
// Usage
function LoginForm() {
const usernameRef = useRef(null)
return (
<>
<SmartInput ref={usernameRef} placeholder="Username" />
<button onClick={() => usernameRef.current.clear()}>Reset</button>
</>
)
}
Note: the deps array of useImperativeHandle works exactly like useEffect —
an empty array means the handle object is created once and never recreated.
Rule of thumb: always keep the raw DOM ref private and return only the surface area the parent actually needs.
Imperative handles suit inherently imperative actions that have no sensible declarative representation: focus management, scroll commands, starting/stopping media, triggering animations, opening/closing portals when the open state is owned by the child.
// ✅ appropriate — "start playing" is an event, not a state the parent owns
player.current.play()
// ✅ appropriate — programmatic focus after async validation
inputRef.current.focus()
// ❌ avoid — visibility is declarative; use a prop instead
modalRef.current.setTitle('New title') // should be <Modal title={...} />
// ❌ avoid — data flow should go through props, not imperative setters
listRef.current.setItems(data) // should be <List items={data} />
React's model is "data flows down via props." Imperative handles are the escape hatch for side-effectful actions that can't be expressed as a render-cycle change.
Rule of thumb: if the parent needs to trigger an action, use a handle; if it needs to change what renders, use a prop.
Forwarding the raw DOM ref gives the parent full access — convenient, but it couples the parent to implementation details and allows accidental mutations.
// raw DOM ref — parent can do anything, including things you don't want
const RawInput = forwardRef((props, ref) => <input ref={ref} {...props} />)
// custom handle — parent can only call what you expose
const SafeInput = forwardRef((props, ref) => {
const inner = useRef(null)
useImperativeHandle(ref, () => ({
focus: () => inner.current.focus(),
// getSelectionRange, scrollIntoView, etc. — only what you choose
}))
return <input ref={inner} {...props} />
})
Reasons to prefer a custom handle:
- The child may replace its DOM implementation (e.g. swap
<input>for a contenteditable div) without breaking the parent. - Prevents parents from reading
valuedirectly, bypassing React's state. - Documents the intended imperative API explicitly.
Rule of thumb: forward the raw ref for thin wrappers; use a custom handle for components with a stable public contract.
forwardRef takes two type parameters: the element / handle type and the
props type. The order is forwardRef<HandleType, PropsType>.
import { forwardRef, useRef, type ForwardedRef } from 'react'
interface InputProps {
label: string
defaultValue?: string
}
// forwardRef<handle-type, props-type>
const LabelledInput = forwardRef<HTMLInputElement, InputProps>(
function LabelledInput({ label, defaultValue }, ref) {
return (
<label>
{label}
<input ref={ref} defaultValue={defaultValue} />
</label>
)
}
)
// Consumer
function Form() {
const ref = useRef<HTMLInputElement>(null)
ref.current?.focus() // TS knows it's HTMLInputElement | null
return <LabelledInput ref={ref} label="Name" />
}
If you use useImperativeHandle, replace HTMLInputElement with your handle
interface (e.g. { focus(): void; clear(): void }).
Rule of thumb: forwardRef<HandleType, PropsType> — handle first, props second.
Define an interface for the handle, use it as the first generic to forwardRef,
and useImperativeHandle will enforce that createHandle returns a conforming
object.
import { forwardRef, useImperativeHandle, useRef } from 'react'
// 1. Define the public imperative API
export interface ModalHandle {
open(): void
close(): void
isOpen(): boolean
}
interface ModalProps {
title: string
children: React.ReactNode
}
// 2. Use ModalHandle as the first generic
const Modal = forwardRef<ModalHandle, ModalProps>(function Modal(
{ title, children },
ref
) {
const [visible, setVisible] = React.useState(false)
useImperativeHandle(ref, () => ({
open: () => setVisible(true),
close: () => setVisible(false),
isOpen: () => visible,
}))
if (!visible) return null
return <dialog open><h2>{title}</h2>{children}</dialog>
})
// 3. Parent types the ref with the handle interface
function App() {
const modalRef = React.useRef<ModalHandle>(null)
return (
<>
<Modal ref={modalRef} title="Alert">Are you sure?</Modal>
<button onClick={() => modalRef.current?.open()}>Show</button>
</>
)
}
Rule of thumb: export the handle interface alongside the component so consumers can type their refs without importing internals.
In React 19, ref is a regular prop. Function components can receive ref
directly in their props object — no forwardRef wrapper required. forwardRef
still works for backwards compatibility but is considered legacy and will eventually
be deprecated.
// React 19 — ref is just another prop
function FancyInput({ ref, ...props }: React.ComponentProps<'input'> & {
ref?: React.Ref<HTMLInputElement>
}) {
return <input ref={ref} className="fancy" {...props} />
}
// React ≤ 18 — must use forwardRef
const FancyInputLegacy = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
function FancyInputLegacy(props, ref) {
return <input ref={ref} className="fancy" {...props} />
}
)
useImperativeHandle continues to work in React 19 — you still call it inside the
component with the ref prop directly.
Rule of thumb: for new React 19 codebases, skip forwardRef and accept ref as a prop; keep forwardRef when targeting older React versions.
Both forwardRef and memo are higher-order wrappers that return a new component.
You must apply forwardRef first (innermost), then wrap with memo so that
memoisation sees the already-forwarded component.
import { forwardRef, memo, useRef } from 'react'
// Step 1 — enable ref forwarding
const BaseInput = forwardRef(function BaseInput(props, ref) {
return <input ref={ref} {...props} />
})
// Step 2 — memoize the forwarded component
const MemoInput = memo(BaseInput)
// Usage — both ref forwarding and memoisation work correctly
function Parent() {
const ref = useRef(null)
return <MemoInput ref={ref} value="" onChange={() => {}} />
}
If you apply memo first and then forwardRef, the outer wrapper still works
in practice (React resolves the chain), but the conventional and explicit order is
memo(forwardRef(...)).
Rule of thumb: memo(forwardRef(component)) — ref first, memo second.
This pattern is appropriate when the open/closed state lives inside the modal itself (e.g. it manages its own transition) and the parent only needs to command it.
const ConfirmModal = forwardRef(function ConfirmModal({ onConfirm }, ref) {
const [open, setOpen] = React.useState(false)
const dialogRef = useRef(null)
useImperativeHandle(ref, () => ({
open: () => { setOpen(true); dialogRef.current?.showModal() },
close: () => { setOpen(false); dialogRef.current?.close() },
}))
if (!open) return null
return (
<dialog ref={dialogRef}>
<p>Are you sure?</p>
<button onClick={() => { onConfirm(); setOpen(false) }}>Yes</button>
<button onClick={() => setOpen(false)}>No</button>
</dialog>
)
})
function DeleteButton({ id }) {
const modal = useRef(null)
return (
<>
<button onClick={() => modal.current.open()}>Delete</button>
<ConfirmModal ref={modal} onConfirm={() => deleteItem(id)} />
</>
)
}
Rule of thumb: use this only when open/close state is truly private to the modal; if the parent must know whether the modal is open, lift state or use a prop instead.
Focus management is the canonical use case — moving focus programmatically after validation errors or submit actions.
const Field = forwardRef(function Field({ label, error, ...inputProps }, ref) {
return (
<div className={error ? 'field field--error' : 'field'}>
<label>{label}</label>
<input ref={ref} {...inputProps} />
{error && <span className="error">{error}</span>}
</div>
)
})
function SignupForm() {
const emailRef = useRef(null)
const passwordRef = useRef(null)
function handleSubmit(e) {
e.preventDefault()
const errors = validate(e.target)
if (errors.email) { emailRef.current.focus(); return }
if (errors.password) { passwordRef.current.focus(); return }
submitForm(e.target)
}
return (
<form onSubmit={handleSubmit}>
<Field ref={emailRef} label="Email" type="email" />
<Field ref={passwordRef} label="Password" type="password" />
<button type="submit">Sign up</button>
</form>
)
}
Rule of thumb: reach for forwardRef when a parent orchestrates focus across several child inputs after async feedback.
Animation libraries (GSAP, Framer Motion) maintain internal timelines. Exposing
play, pause, and reset via a handle keeps the animation logic encapsulated
while giving parent components a clean trigger surface.
import gsap from 'gsap'
const AnimatedCard = forwardRef(function AnimatedCard({ children }, ref) {
const cardRef = useRef(null)
const tlRef = useRef(null) // private timeline
useEffect(() => {
tlRef.current = gsap.timeline({ paused: true })
.fromTo(cardRef.current,
{ opacity: 0, y: 40 },
{ opacity: 1, y: 0, duration: 0.4 }
)
}, [])
useImperativeHandle(ref, () => ({
play: () => tlRef.current.play(),
pause: () => tlRef.current.pause(),
reset: () => tlRef.current.restart().pause(),
}))
return <div ref={cardRef}>{children}</div>
})
function Demo() {
const card = useRef(null)
return (
<>
<AnimatedCard ref={card}>Hello</AnimatedCard>
<button onClick={() => card.current.play()}>Animate in</button>
</>
)
}
Rule of thumb: expose semantic action names (play, pause) not raw library handles — the parent shouldn't need to know which animation library you use.
React.ForwardedRef<T> is the exact type of the second argument received by a
forwardRef render function. It is a union: RefCallback<T> | MutableRefObject<T> | null.
React.Ref<T> is a wider union that also includes string refs (legacy) and is
used for passing refs to elements.
import { forwardRef, type ForwardedRef } from 'react'
// ForwardedRef<T> is correct for the parameter you *receive*
function BaseInput(
props: React.ComponentProps<'input'>,
ref: ForwardedRef<HTMLInputElement> // not Ref<> — this is what forwardRef gives you
) {
return <input ref={ref} {...props} />
}
export default forwardRef(BaseInput)
// If you want to accept a ref in a helper that isn't a forwardRef component:
function mergeRefs<T>(...refs: ForwardedRef<T>[]): React.RefCallback<T> {
return (node) => {
refs.forEach(ref => {
if (typeof ref === 'function') ref(node)
else if (ref) ref.current = node
})
}
}
Rule of thumb: use ForwardedRef<T> for the ref parameter inside forwardRef; use Ref<T> or RefObject<T> in props interfaces for refs the component accepts as props.
Use React.createRef() (or useRef inside a wrapper component) to capture the
handle, then assert on its methods.
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { createRef } from 'react'
import SmartInput from './SmartInput'
test('clear() empties the input', async () => {
const ref = createRef()
render(<SmartInput ref={ref} defaultValue="hello" data-testid="inp" />)
// verify initial state
expect(screen.getByTestId('inp').value).toBe('hello')
// call the imperative method
ref.current.clear()
// assert DOM changed
expect(screen.getByTestId('inp').value).toBe('')
})
test('focus() moves focus to the input', () => {
const ref = createRef()
render(<SmartInput ref={ref} data-testid="inp" />)
ref.current.focus()
expect(screen.getByTestId('inp')).toHaveFocus()
})
You don't need to test the internal DOM structure — only that the exposed handle methods produce the expected observable behaviour.
Rule of thumb: test the contract of the handle (what it does), not how it is implemented.
useImperativeHandle(ref, createHandle, deps) re-runs createHandle whenever a
dependency changes, replacing ref.current with a new object. With an empty array
[], the handle is created once. With no deps array at all, it reruns on every render.
const Counter = forwardRef(function Counter(props, ref) {
const [count, setCount] = useState(0)
useImperativeHandle(ref, () => ({
increment: () => setCount(c => c + 1),
// getCount captures the current count via closure —
// must be in deps so the handle refreshes when count changes
getCount: () => count,
}), [count]) // ← re-create handle when count changes
return <p>{count}</p>
})
If getCount were in a stale closure (deps omitted or []) it would always return
the initial value. Omitting deps entirely causes a new handle object every render,
which is wasteful but not incorrect.
Rule of thumb: include any state or prop that the handle methods read via closure; use [] only when the methods rely solely on refs or setters (which are stable).
A DOM node can only have one ref prop. When a component needs its own local ref
and must forward the parent's ref to the same node, you merge them with a
callback ref.
function mergeRefs(...refs) {
return (node) => {
refs.forEach(ref => {
if (typeof ref === 'function') ref(node) // callback ref
else if (ref != null) ref.current = node // object ref
})
}
}
const ScrollableList = forwardRef(function ScrollableList(props, forwardedRef) {
const localRef = useRef(null) // needed for internal scroll logic
function scrollToTop() {
localRef.current?.scrollTo({ top: 0, behavior: 'smooth' })
}
return (
<ul
ref={mergeRefs(localRef, forwardedRef)} // both refs point at <ul>
{...props}
/>
)
})
Libraries like useMergedRef (react-merge-refs) do exactly this. React 19's
cleanup-returning ref callbacks make the pattern cleaner still.
Rule of thumb: when you need two refs on one node, always merge — never pick one and lose the other.
Five common mistakes:
- Exposing the raw DOM node when a custom handle is safer — gives the parent unrestricted access.
- Using an imperative handle to pass data —
ref.current.getUser()should beconst { user } = useUser()or a prop. - Forgetting to memoize the handle when it closes over frequently-changing state without listing it in deps.
- Wrapping every component in
forwardRef"just in case" — most components are controlled via props and don't need a ref at all. - Not cleaning up — if the handle exposes subscriptions or timers, they should
be cleared in a
useEffectcleanup, not in the handle method itself.
// ❌ anti-pattern — data fetch through an imperative handle
const DataList = forwardRef((_, ref) => {
useImperativeHandle(ref, () => ({
getData: async () => fetchData(), // parent should just receive data as a prop
}))
return <ul />
})
// ✅ prefer — data flows through props / context
function DataList({ data }) {
return <ul>{data.map(item => <li key={item.id}>{item.name}</li>)}</ul>
}
Rule of thumb: before adding an imperative handle, ask "can I achieve this with a prop, callback, or state?" — if yes, do that instead.
React DevTools shows the display name in the component tree. Anonymous forwardRef
calls appear as ForwardRef — unhelpful for debugging.
// Option 1 — named function expression (preferred)
const TextInput = forwardRef(function TextInput(props, ref) {
return <input ref={ref} {...props} />
})
// DevTools shows: TextInput
// Option 2 — set displayName explicitly
const TextInput2 = forwardRef((props, ref) => (
<input ref={ref} {...props} />
))
TextInput2.displayName = 'TextInput'
// Option 3 — in a factory helper, set displayName dynamically
function createForwardedInput(displayName) {
const C = forwardRef((props, ref) => <input ref={ref} {...props} />)
C.displayName = displayName
return C
}
Named function expressions (Option 1) are the cleanest because the name is inferred automatically without an extra line.
Rule of thumb: always use a named function inside forwardRef — you get a readable DevTools tree for free.
A list component that manages its own DOM can expose a scrollTo(index) method
so the parent doesn't have to reach into its internals.
const VirtualList = forwardRef(function VirtualList({ items }, ref) {
const rowRefs = useRef([]) // array of row DOM nodes
useImperativeHandle(ref, () => ({
// scrollTo(index) — brings row at `index` into the viewport
scrollTo(index) {
rowRefs.current[index]?.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
})
},
}), []) // stable — no deps needed; rowRefs.current is mutable
return (
<ul>
{items.map((item, i) => (
<li key={item.id} ref={el => { rowRefs.current[i] = el }}>
{item.label}
</li>
))}
</ul>
)
})
function SearchResults({ results, activeIndex }) {
const listRef = useRef(null)
useEffect(() => {
listRef.current?.scrollTo(activeIndex)
}, [activeIndex])
return <VirtualList ref={listRef} items={results} />
}
Rule of thumb: when the child owns a collection of DOM nodes, expose a semantic scrollTo(index) rather than exposing the raw refs array.
More Patterns interview questions
More ways to practice
The self-quiz is live. Get notified when mock interviews and new question packs drop.