React 19 Refs as Props: The Complete Guide
A comprehensive, beginner-friendly guide to React 19's biggest ref change: passing ref as a regular prop. Covers how refs work, forwardRef vs the new approach, practical before-and-after examples, common mistakes, and migration tips.
React 19 shipped one of the most developer-friendly quality-of-life improvements in years: ref is now a regular prop. That single sentence might not sound dramatic, but it eliminates an entire API โ forwardRef โ that has confused React developers since it was introduced. If you have ever stared at a forwardRef wrapper and wondered why it exists, this guide is for you. We will start from the very basics of how refs work, explain what changed in React 19, walk through real before-and-after code, and finish with migration advice and an FAQ.
What Is a Ref and Why Do We Need One?
In React, the UI is driven by state and props. You describe what the screen should look like, and React updates the DOM for you. Most of the time you never need to touch the DOM directly. But sometimes you do โ focusing an input field, measuring an element's size, triggering an animation library, or integrating a third-party widget. That is exactly what a ref is for.
A ref (short for reference) is a special object with a single property called current. React sets current to the underlying DOM node (or component instance) after the component mounts, and resets it to null when the component unmounts. Crucially, changing a ref does NOT trigger a re-render, which makes refs perfect for storing mutable values that should not affect the UI.
- Focusing or blurring an input field programmatically
- Reading the scroll position or dimensions of a DOM element
- Triggering imperative animations (e.g., GSAP, Framer Motion imperative API)
- Integrating non-React libraries that need a real DOM node
- Storing a mutable value across renders without causing re-renders (like a timer ID)
import { useRef } from 'react';
export default function FocusInput() {
// Create a ref โ initially { current: null }
const inputRef = useRef<HTMLInputElement>(null);
function handleClick() {
// Access the real DOM node and call .focus()
inputRef.current?.focus();
}
return (
<>
{/* Attach the ref to the DOM element via the special ref prop */}
<input ref={inputRef} type="text" placeholder="Click the button..." />
<button onClick={handleClick}>Focus the input</button>
</>
);
}In the example above, useRef creates the ref object, and we attach it to the <input> element using the built-in ref prop. React fills in inputRef.current with the actual DOM node after the component mounts. Simple enough โ but what happens when you want to pass a ref down to a child component?
The Old Way: Why forwardRef Existed in React 18 and Earlier
Before React 19, ref was NOT a regular prop. React treated it as a reserved keyword โ just like key. If you tried to pass ref={myRef} to a custom component, React would silently swallow it. The child component would never receive it in its props object. This was a deliberate design decision in early React, but it created a painful problem: how do you give a parent access to a DOM node inside a child component?
The answer was forwardRef โ a higher-order function that wraps your component and explicitly threads the ref through as a second argument alongside props. Here is what that looked like:
// React 18 and earlier โ the forwardRef pattern
import { forwardRef, useRef } from 'react';
// โ
You MUST wrap the component in forwardRef to receive a ref from the parent
const FancyInput = forwardRef<HTMLInputElement, { placeholder?: string }>(
function FancyInput({ placeholder }, ref) {
// ref is the second argument, NOT part of props
return (
<input
ref={ref}
className="fancy-input"
placeholder={placeholder}
/>
);
}
);
// Parent component
export default function Form() {
const inputRef = useRef<HTMLInputElement>(null);
return (
<>
<FancyInput ref={inputRef} placeholder="Type here..." />
<button onClick={() => inputRef.current?.focus()}>Focus</button>
</>
);
}This works, but it has real drawbacks. You must remember to wrap every component that might ever receive a ref. The forwardRef call adds visual noise. TypeScript generics get verbose. DevTools show an anonymous ForwardRef wrapper in the component tree. And if you forget the wrapper, the bug is silent โ the ref just doesn't work, with no error message.
"forwardRef is a workaround. It exists because ref was never a real prop. React 19 fixes the root cause instead of patching around it." โ React Core Team (React 19 release notes)
What Changed in React 19: ref Is Now a Regular Prop
React 19 removes the special-case treatment of ref. It is now passed to function components as part of the normal props object โ just like className, onClick, or any other prop you define. No wrapper. No second argument. No ceremony.
Here is the exact same FancyInput component rewritten for React 19:
// React 19 โ ref is just a prop!
import { useRef, type Ref } from 'react';
interface FancyInputProps {
placeholder?: string;
ref?: Ref<HTMLInputElement>; // Declare ref in your props interface like any other prop
}
// No forwardRef wrapper needed โ ref arrives in props directly
function FancyInput({ placeholder, ref }: FancyInputProps) {
return (
<input
ref={ref}
className="fancy-input"
placeholder={placeholder}
/>
);
}
// Parent component โ usage is identical to before
export default function Form() {
const inputRef = useRef<HTMLInputElement>(null);
return (
<>
<FancyInput ref={inputRef} placeholder="Type here..." />
<button onClick={() => inputRef.current?.focus()}>Focus</button>
</>
);
}The parent-side usage (passing ref={inputRef}) looks identical. The big change is inside FancyInput โ it is now a plain function with no wrapper. The ref prop is declared in the TypeScript interface just like any other prop, and destructured from the props argument. That's it.
| Feature | React 18 (forwardRef) | React 19 (ref as prop) |
| Wrapper required? | Yes โ forwardRef() | No |
| ref in props object? | No โ second argument | Yes โ part of props |
| TypeScript verbosity | High โ two generics | Low โ one interface |
| DevTools display | Shows ForwardRef wrapper | Shows component name directly |
| Forgetting the wrapper | Silent failure (ref is null) | N/A โ no wrapper needed |
| Class components | Not affected | Not affected (use createRef) |
Practical Before-and-After Examples
Let's look at a few real-world scenarios side by side so the difference is crystal clear.
Example 1 โ A reusable Button component that exposes its DOM node:
// โ BEFORE โ React 18
import { forwardRef, type ButtonHTMLAttributes } from 'react';
const Button = forwardRef<HTMLButtonElement, ButtonHTMLAttributes<HTMLButtonElement>>(
function Button({ children, ...rest }, ref) {
return (
<button ref={ref} className="btn" {...rest}>
{children}
</button>
);
}
);
export default Button;// โ
AFTER โ React 19
import { type ButtonHTMLAttributes, type Ref } from 'react';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
ref?: Ref<HTMLButtonElement>;
}
function Button({ children, ref, ...rest }: ButtonProps) {
return (
<button ref={ref} className="btn" {...rest}>
{children}
</button>
);
}
export default Button;Example 2 โ A custom modal that needs to be focused when it opens:
// โ
React 19 โ Modal with ref forwarding, no forwardRef needed
import { useEffect, useRef, type Ref } from 'react';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
ref?: Ref<HTMLDivElement>;
}
function Modal({ isOpen, onClose, ref }: ModalProps) {
if (!isOpen) return null;
return (
<div
ref={ref}
role="dialog"
aria-modal="true"
tabIndex={-1} // Makes the div focusable
className="modal-overlay"
>
<div className="modal-content">
<button onClick={onClose}>Close</button>
<p>Modal content goes here.</p>
</div>
</div>
);
}
// Parent โ focuses the modal div when it opens
export default function App() {
const [open, setOpen] = useState(false);
const modalRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (open) {
modalRef.current?.focus(); // Move keyboard focus into the modal
}
}, [open]);
return (
<>
<button onClick={() => setOpen(true)}>Open Modal</button>
<Modal ref={modalRef} isOpen={open} onClose={() => setOpen(false)} />
</>
);
}When Should You Actually Use Refs?
Refs are a powerful escape hatch, but they should not be your first tool. React's declarative model โ state drives the UI โ handles the vast majority of use cases more cleanly. Reach for a ref only when you genuinely need to step outside that model.
- โ Managing focus, text selection, or media playback (e.g., video.play())
- โ Triggering imperative animations that need direct DOM access
- โ Integrating third-party libraries (maps, charts, rich-text editors) that mount into a DOM node
- โ Storing a mutable value (like a setInterval ID) that should NOT cause re-renders
- โ Reading or writing data that should be displayed in the UI โ use state instead
- โ Passing data between components โ use props or context instead
- โ Controlling whether something is visible โ use conditional rendering instead
A good rule of thumb: if changing the value should update what the user sees, use useState. If it should not, a ref might be the right choice.
Common Mistakes to Avoid With Refs
Even with the simpler React 19 API, there are several pitfalls that trip up developers. Here are the most common ones and how to avoid them.
- Reading ref.current during render โ The ref is populated after the component mounts. Reading it during the render phase (outside useEffect or event handlers) will give you null.
- Forgetting to declare ref in the TypeScript interface โ In React 19, TypeScript won't know about ref unless you add it to your props interface. Without it, you'll get a type error when the parent tries to pass ref.
- Using ref on class components the old way โ Class components are not affected by this change. They still use createRef() or callback refs. The new prop-based approach is for function components only.
- Mutating ref.current and expecting a re-render โ Refs are intentionally non-reactive. If you need the UI to update, put the value in state.
- Passing ref to a component that doesn't forward it โ If a child component doesn't accept and attach the ref prop to a DOM element, the parent's ref.current will remain null. Always verify the child wires it up.
- Using forwardRef in React 19 unnecessarily โ forwardRef still works in React 19 (it's not removed), but it's deprecated. New code should use the plain prop pattern. Mixing both styles in a codebase creates confusion.
// โ Mistake: reading ref.current during render
function BadComponent({ ref }: { ref?: React.Ref<HTMLDivElement> }) {
// ๐จ ref.current is null here โ the DOM doesn't exist yet during render
console.log((ref as React.RefObject<HTMLDivElement>)?.current); // null
return <div ref={ref}>Hello</div>;
}
// โ
Correct: read ref.current inside useEffect or an event handler
function GoodComponent({ ref }: { ref?: React.Ref<HTMLDivElement> }) {
useEffect(() => {
// โ
DOM is mounted โ ref.current is the real element
console.log((ref as React.RefObject<HTMLDivElement>)?.current);
}, []);
return <div ref={ref}>Hello</div>;
}Migrating From forwardRef to the React 19 Prop Pattern
React 19 keeps forwardRef working โ it is deprecated, not removed. That means your existing code will not break when you upgrade. You can migrate gradually, component by component, at your own pace. Here is a simple migration checklist:
- Upgrade to React 19 and @types/react 19 (or the built-in types if using the new package).
- Find all forwardRef usages in your codebase: grep -r 'forwardRef' src/
- For each component, unwrap the forwardRef call and convert the second ref argument into a named prop in the props interface.
- Add ref?: Ref<T> to the component's TypeScript interface.
- Destructure ref from props alongside the other props.
- Run your test suite. The parent-side usage (ref={myRef}) does not change, so most tests should pass without modification.
- Enable the React 19 ESLint rule (react/no-forward-ref) to catch any remaining forwardRef usage.
# Step 1: Upgrade React and types
npm install react@19 react-dom@19
npm install --save-dev @types/react@19 @types/react-dom@19
# Step 2: Find all forwardRef usages
grep -rn 'forwardRef' src/The React team also provides an official codemod to automate the migration. Run it on your source directory and it will rewrite forwardRef patterns to the new prop style automatically:
# Official React 19 codemod (requires @next/codemod or react-codemod)
npx codemod@latest react/19/replace-use-form-state
# For the forwardRef-specific transform:
npx react-codemod remove-forward-ref src/Frequently Asked Questions
Here are answers to the questions developers ask most often about React 19 refs.
- Q: Is forwardRef removed in React 19? โ No. It is deprecated but still works. You will see a console warning in development mode. It will likely be removed in a future major version.
- Q: Does this change affect class components? โ No. Class components use createRef() and the ref prop has always worked differently for them. This change only affects function components.
- Q: Can I still use callback refs? โ Yes. A callback ref (ref={node => { ... }}) is just a function, and it works the same way as before. You can pass it as a prop in React 19 without any changes.
- Q: What about useImperativeHandle? โ useImperativeHandle still works in React 19. You use it inside the child component to expose a custom object on the ref instead of the raw DOM node. You no longer need forwardRef to wrap it.
- Q: Will TypeScript complain if I don't add ref to my props interface? โ Yes. If a parent tries to pass ref={myRef} to your component and ref is not in the interface, TypeScript will throw a type error. Always declare ref?: Ref<T> in your interface if the component should accept one.
- Q: Is the key prop also a regular prop now? โ No. key remains a reserved React concept and is not accessible inside components as a prop. Only ref changed.
// useImperativeHandle in React 19 โ no forwardRef needed
import { useImperativeHandle, useRef, type Ref } from 'react';
interface VideoPlayerHandle {
play: () => void;
pause: () => void;
}
interface VideoPlayerProps {
src: string;
ref?: Ref<VideoPlayerHandle>;
}
function VideoPlayer({ src, ref }: VideoPlayerProps) {
const videoRef = useRef<HTMLVideoElement>(null);
// Expose only play/pause to the parent โ not the entire DOM node
useImperativeHandle(ref, () => ({
play: () => videoRef.current?.play(),
pause: () => videoRef.current?.pause(),
}));
return <video ref={videoRef} src={src} />;
}
// Parent
export default function App() {
const playerRef = useRef<VideoPlayerHandle>(null);
return (
<>
<VideoPlayer ref={playerRef} src="/movie.mp4" />
<button onClick={() => playerRef.current?.play()}>Play</button>
<button onClick={() => playerRef.current?.pause()}>Pause</button>
</>
);
}Next Steps and Key Takeaways
React 19's ref-as-prop change is a small API surface with a big ergonomic payoff. It removes an entire concept (forwardRef) that developers had to learn, remember, and apply correctly. The mental model is now simpler: ref is just a prop. Pass it like any other prop. Declare it in your interface. Attach it to a DOM element. Done.
- Refs give you a direct handle to a DOM node without triggering re-renders.
- Before React 19, passing a ref to a child component required wrapping it in forwardRef.
- React 19 makes ref a regular prop โ no wrapper, no second argument, less boilerplate.
- forwardRef is deprecated but not removed; migrate gradually using the official codemod.
- Always declare ref?: Ref<T> in your TypeScript props interface when a component should accept one.
- Use refs only for imperative DOM operations; prefer state and props for everything else.
- useImperativeHandle still works in React 19 and no longer requires forwardRef.
To go deeper, explore the official React 19 upgrade guide, experiment with useImperativeHandle for exposing custom APIs from child components, and look into React's new use() hook and Server Components โ both of which shipped alongside this ref improvement in React 19.