React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

react-19refforwardRefpropsdom-access

Ref as Prop - Pass refs as regular props without forwardRef

Recipe

// React 19: ref is just a prop
function TextInput({ placeholder, ref }: { placeholder: string; ref?: React.Ref<HTMLInputElement> }) {
  return <input placeholder={placeholder} ref={ref} />;
}
 
// Usage -- identical to before
import { useRef } from "react";
 
function App() {
  const inputRef = useRef<HTMLInputElement>(null);
 
  return (
    <div>
      <TextInput ref={inputRef} placeholder="Type here..." />
      <button onClick={() => inputRef.current?.focus()}>Focus</button>
    </div>
  );
}

When to reach for this: Use this pattern for any function component that needs to expose a DOM node or imperative handle to a parent. It replaces forwardRef entirely.

Working Example

// A reusable input component library with ref support
"use client";
 
import { useRef, useImperativeHandle, type Ref, type ReactNode } from "react";
 
// Simple ref forwarding -- no forwardRef wrapper needed
function Input({
  label,
  error,
  ref,
  ...inputProps
}: {
  label: string;
  error?: string;
  ref?: Ref<HTMLInputElement>;
} & React.InputHTMLAttributes<HTMLInputElement>) {
  return (
    <div className="flex flex-col gap-1">
      <label className="text-sm font-medium">{label}</label>
      <input
        ref={ref}
        className={`border rounded p-2 ${error ? "border-red-500" : "border-gray-300"}`}
        {...inputProps}
      />
      {error && <span className="text-red-500 text-sm">{error}</span>}
    </div>
  );
}
 
// Imperative handle with ref -- also no forwardRef needed
type ComboboxHandle = {
  open: () => void;
  close: () => void;
  reset: () => void;
};
 
function Combobox({
  options,
  ref,
}: {
  options: string[];
  ref?: Ref<ComboboxHandle>;
}) {
  const inputRef = useRef<HTMLInputElement>(null);
  const [isOpen, setIsOpen] = useState(false);
  const [value, setValue] = useState("");
 
  useImperativeHandle(ref, () => ({
    open: () => setIsOpen(true),
    close: () => setIsOpen(false),
    reset: () => {
      setValue("");
      setIsOpen(false);
    },
  }));
 
  return (
    <div className="relative">
      <input
        ref={inputRef}
        value={value}
        onChange={(e) => setValue(e.target.value)}
        onFocus={() => setIsOpen(true)}
      />
      {isOpen && (
        <ul className="absolute top-full left-0 border bg-white w-full">
          {options
            .filter((o) => o.toLowerCase().includes(value.toLowerCase()))
            .map((option) => (
              <li
                key={option}
                onClick={() => {
                  setValue(option);
                  setIsOpen(false);
                }}
                className="p-2 hover:bg-gray-100 cursor-pointer"
              >
                {option}
              </li>
            ))}
        </ul>
      )}
    </div>
  );
}
 
// Parent using both components
import { useState } from "react";
 
export default function RegistrationForm() {
  const nameRef = useRef<HTMLInputElement>(null);
  const comboRef = useRef<ComboboxHandle>(null);
 
  function handleReset() {
    nameRef.current?.focus();
    comboRef.current?.reset();
  }
 
  return (
    <form className="space-y-4">
      <Input ref={nameRef} label="Full Name" placeholder="Jane Doe" />
      <Combobox ref={comboRef} options={["Engineering", "Design", "Product", "Marketing"]} />
      <button type="button" onClick={handleReset}>
        Reset & Focus Name
      </button>
    </form>
  );
}

What this demonstrates:

  • A simple Input component receiving ref as a regular prop and forwarding it to the native <input>
  • A Combobox component using useImperativeHandle with ref as a prop to expose custom methods
  • Parent components using useRef to interact with child DOM nodes and imperative handles
  • No forwardRef wrapper anywhere

Deep Dive

How It Works

  • In React 19, ref is treated as a regular prop for function components. React no longer strips it out before passing props to the component.
  • forwardRef still works and is not removed, but it is deprecated. A future React version will remove it. React logs a deprecation warning in development mode.
  • The ref prop is included in the component's props type automatically when using React.ComponentProps<typeof MyComponent>.
  • Class components continue to use ref as before -- the ref points to the class instance. This change only affects function components.
  • useImperativeHandle(ref, createHandle) continues to work exactly as before. The only difference is where ref comes from -- a prop instead of the second argument of forwardRef.

Variations

Ref cleanup functions (new in React 19):

function MeasuredDiv({ ref }: { ref?: Ref<HTMLDivElement> }) {
  return (
    <div
      ref={(node) => {
        // Setup
        if (node) {
          console.log("Mounted:", node.getBoundingClientRect());
        }
        // Return cleanup function (new in React 19)
        return () => {
          console.log("Unmounted");
        };
      }}
    >
      Measured content
    </div>
  );
}

Generic component with ref:

type ListProps<T> = {
  items: T[];
  renderItem: (item: T) => ReactNode;
  ref?: Ref<HTMLUListElement>;
};
 
function List<T>({ items, renderItem, ref }: ListProps<T>) {
  return (
    <ul ref={ref}>
      {items.map((item, i) => (
        <li key={i}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

Migration from forwardRef:

// Before (React 18)
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
  return <input ref={ref} {...props} />;
});
 
// After (React 19)
function Input({ ref, ...props }: InputProps & { ref?: Ref<HTMLInputElement> }) {
  return <input ref={ref} {...props} />;
}

TypeScript Notes

  • Use Ref<T> (not RefObject<T>) for the prop type: ref?: React.Ref<HTMLInputElement>.
  • React.Ref<T> is a union of RefCallback<T>, RefObject<T>, and null. This covers all ref usage patterns.
  • When using ComponentProps, the ref prop is included automatically in React 19 types.
  • For imperative handles, type the ref as Ref<MyHandle> where MyHandle is your custom interface.

Gotchas

  • forwardRef deprecation warning in dev -- Existing forwardRef code works but logs a warning. Fix: Migrate by moving ref from the second forwardRef argument into the props object. Run the React codemod: npx @react-codemod/cli forward-ref-as-prop.
  • Spreading props may pass ref unintentionally -- {...props} now includes ref if the parent passed one. Fix: Destructure ref explicitly: const { ref, ...rest } = props.
  • Ref cleanup return value -- In React 19, ref callbacks can return a cleanup function. If your existing ref callback returns something other than a function or undefined, React will warn. Fix: Ensure ref callbacks either return nothing or return a cleanup function.
  • Class component refs unchanged -- ref on a class component still points to the instance, not a prop. Fix: No migration needed for class components.
  • Third-party component libraries -- Libraries still using forwardRef internally continue to work. You do not need to wait for them to migrate. Fix: No action required.

Alternatives

ApproachWhen to choose
ref as prop (React 19)Default for all new function components
forwardRef (deprecated)Existing code that has not been migrated yet
useImperativeHandleWhen you need to expose a custom API instead of a DOM node
Callback refWhen you need to run logic on mount/unmount of the ref target
State-based communicationWhen parent-child communication should trigger re-renders instead of imperative calls

FAQs

Why was forwardRef removed in favor of ref as a prop?
  • forwardRef added unnecessary wrapper complexity and made component APIs harder to understand
  • In React 19, ref is treated as a regular prop for function components -- React no longer strips it out
  • forwardRef still works but is deprecated and will be removed in a future version
How do you migrate from forwardRef to ref-as-prop?
// Before (React 18)
const Input = forwardRef<HTMLInputElement, Props>((props, ref) => {
  return <input ref={ref} {...props} />;
});
 
// After (React 19)
function Input({ ref, ...props }: Props & { ref?: Ref<HTMLInputElement> }) {
  return <input ref={ref} {...props} />;
}
Does useImperativeHandle still work with ref as a prop?
  • Yes. useImperativeHandle(ref, createHandle) works exactly as before
  • The only difference is that ref comes from props instead of the second argument of forwardRef
  • Use it to expose custom methods (e.g., open(), close(), reset()) to parent components
What are ref cleanup functions in React 19?
<div ref={(node) => {
  if (node) console.log("Mounted:", node);
  // Return cleanup function (new in React 19)
  return () => console.log("Unmounted");
}} />

Ref callbacks can now return a cleanup function, similar to useEffect cleanup.

Does ref-as-prop affect class components?
  • No. Class components continue to use ref as before -- the ref points to the class instance
  • This change only affects function components
  • No migration is needed for class components
Is there an automated codemod for migrating forwardRef?
  • Yes. Run npx @react-codemod/cli forward-ref-as-prop to automatically migrate
  • It moves ref from the forwardRef second argument into the props destructuring
  • Review the output for any edge cases in your codebase
How do you write a generic component that accepts a ref?
type ListProps<T> = {
  items: T[];
  renderItem: (item: T) => ReactNode;
  ref?: Ref<HTMLUListElement>;
};
 
function List<T>({ items, renderItem, ref }: ListProps<T>) {
  return (
    <ul ref={ref}>
      {items.map((item, i) => <li key={i}>{renderItem(item)}</li>)}
    </ul>
  );
}
Gotcha: What happens when you spread props that include ref?
  • {...props} now includes ref if the parent passed one, which may forward it unintentionally
  • Always destructure ref explicitly: const { ref, ...rest } = props
  • Then apply ref only where intended
Gotcha: My ref callback returns a value and React is warning. Why?
  • In React 19, ref callbacks can return a cleanup function or undefined
  • Returning anything else (e.g., a non-function value) triggers a warning
  • Ensure your ref callback either returns nothing or explicitly returns a cleanup function
What is the correct TypeScript type for the ref prop?
  • Use React.Ref<T> (not RefObject<T>) for the prop type: ref?: React.Ref<HTMLInputElement>
  • React.Ref<T> is a union of RefCallback<T>, RefObject<T>, and null
  • This covers callback refs, object refs, and null
How does ComponentProps work with ref in React 19 TypeScript types?
  • React.ComponentProps<typeof MyComponent> automatically includes ref in the type
  • You do not need to add ref manually when extending component props
  • This is a change from React 18 where ref was excluded from ComponentProps
Do third-party libraries using forwardRef still work in React 19?
  • Yes. Libraries still using forwardRef internally continue to work without issues
  • You do not need to wait for libraries to migrate before upgrading your own code
  • forwardRef is deprecated but functional
  • Overview -- Full list of React 19 features
  • Form Actions -- Forms often use refs for reset and focus
  • Metadata -- Another API simplification in React 19