Skip to content
migrating-from-forwardref logo

Migrating from forwardRef to Ref as Prop

migrating-from-forwardref

Teaches migration from forwardRef to ref-as-prop pattern in React 19. Use when seeing forwardRef usage, upgrading React components, or when refs are mentioned. forwardRef is deprecated in React 19.

SKILL.md

Full skill instructions

Migrating from forwardRef to Ref as Prop

<role> This skill teaches you how to migrate from the deprecated `forwardRef` API to React 19's ref-as-prop pattern. </​role> <when-to-activate> This skill activates when:
  • User mentions forwardRef, refs, or ref forwarding
  • Seeing code that uses React.forwardRef
  • Upgrading components to React 19
  • Need to expose DOM refs from custom components
  • TypeScript errors about ref props </​when-to-activate>
<overview> React 19 deprecates `forwardRef` in favor of ref as a regular prop:

Why the Change:

  1. Simpler API - Refs are just props, no special wrapper needed
  2. Better TypeScript - Easier type inference and typing
  3. Consistency - All props handled the same way
  4. Less Boilerplate - Fewer imports and wrapper functions

Migration Path:

  • forwardRef still works in React 19 (deprecated, not removed)
  • New code should use ref as prop
  • Gradual migration recommended for existing codebases

Key Difference:

// OLD: forwardRef (deprecated)
const Button = forwardRef((props, ref) => ...);

// NEW: ref as prop (React 19)
function Button({ ref, ...props }) { ... }

</​overview>

<workflow> ## Migration Process

Step 1: Identify forwardRef Usage

Search codebase for forwardRef:

# Use Grep tool
pattern: "forwardRef"
output_mode: "files_with_matches"

Step 2: Understand Current Pattern

Before (React 18):

import { forwardRef } from 'react';

const MyButton = forwardRef((props, ref) => {
  return (
    <button ref={ref} className={props.className}>
      {props.children}
    </​button>
  );
});

Step 3: Convert to Ref as Prop

After (React 19):

function MyButton({ children, className, ref }) {
  return (
    <button ref={ref} className={className}>
      {children}
    </​button>
  );
}

Step 4: Update TypeScript Types (if applicable)

Before:

import { forwardRef } from 'react';

interface ButtonProps {
  variant: 'primary' | 'secondary';
  children: React.ReactNode;
}

const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ variant, children }, ref) => {
    return (
      <button ref={ref} className={variant}>
        {children}
      </​button>
    );
  }
);

After:

import { Ref } from 'react';

interface ButtonProps {
  variant: 'primary' | 'secondary';
  children: React.ReactNode;
  ref?: Ref<HTMLButtonElement>;
}

function Button({ variant, children, ref }: ButtonProps) {
  return (
    <button ref={ref} className={variant}>
      {children}
    </​button>
  );
}

Step 5: Test Component

Verify ref forwarding still works:

function Parent() {
  const buttonRef = useRef(null);

  useEffect(() => {
    buttonRef.current?.focus();
  }, []);

  return <Button ref={buttonRef}>Click me</​Button>;
}

</​workflow>

<conditional-workflows> ## Complex Scenarios

If component uses useImperativeHandle:

Before:

const FancyInput = forwardRef((props, ref) => {
  const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
    clear: () => { inputRef.current.value = ''; }
  }));

  return <input ref={inputRef} />;
});

After:

function FancyInput({ ref }) {
  const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
    clear: () => { inputRef.current.value = ''; }
  }));

  return <input ref={inputRef} />;
}

If component has multiple refs:

function ComplexComponent({ ref, innerRef, ...props }) {
  return (
    <div ref={ref}>
      <input ref={innerRef} {...props} />
    </​div>
  );
}

If using generic components:

interface GenericProps<T> {
  value: T;
  ref?: Ref<HTMLDivElement>;
}

function GenericComponent<T>({ value, ref }: GenericProps<T>) {
  return <div ref={ref}>{String(value)}</​div>;
}

</​conditional-workflows>

<progressive-disclosure> ## Reference Files

For detailed information:

  • Ref Cleanup Functions: See ../​../​../​research/​react-19-comprehensive.md (lines 1013-1033)
  • useImperativeHandle: See ../​../​../​research/​react-19-comprehensive.md (lines 614-623)
  • TypeScript Migration: See ../​../​../​research/​react-19-comprehensive.md (lines 890-916)
  • Complete Migration Guide: See ../​../​../​research/​react-19-comprehensive.md (lines 978-1011)

Load references when specific patterns are needed. </​progressive-disclosure>

<examples> ## Example 1: Simple Button Migration

Before (React 18 with forwardRef):

import { forwardRef } from 'react';

const Button = forwardRef((props, ref) => (
  <button ref={ref} {...props}>
    {props.children}
  </​button>
));

Button.displayName = 'Button';

export default Button;

After (React 19 with ref prop):

function Button({ children, ref, ...props }) {
  return (
    <button ref={ref} {...props}>
      {children}
    </​button>
  );
}

export default Button;

Changes Made:

  1. ✅ Removed forwardRef import
  2. ✅ Removed forwardRef wrapper
  3. ✅ Added ref to props destructuring
  4. ✅ Removed unnecessary displayName
  5. ✅ Simplified function signature

Example 2: TypeScript Component with Multiple Props

Before:

import { forwardRef, HTMLAttributes } from 'react';

interface CardProps extends HTMLAttributes<HTMLDivElement> {
  title: string;
  description?: string;
  variant?: 'default' | 'outlined';
}

const Card = forwardRef<HTMLDivElement, CardProps>(
  ({ title, description, variant = 'default', ...props }, ref) => {
    return (
      <div ref={ref} className={`card card-${variant}`} {...props}>
        <h3>{title}</​h3>
        {description && <p>{description}</​p>}
      </​div>
    );
  }
);

Card.displayName = 'Card';

export default Card;

After:

import { Ref, HTMLAttributes } from 'react';

interface CardProps extends HTMLAttributes<HTMLDivElement> {
  title: string;
  description?: string;
  variant?: 'default' | 'outlined';
  ref?: Ref<HTMLDivElement>;
}

function Card({
  title,
  description,
  variant = 'default',
  ref,
  ...props
}: CardProps) {
  return (
    <div ref={ref} className={`card card-${variant}`} {...props}>
      <h3>{title}</​h3>
      {description && <p>{description}</​p>}
    </​div>
  );
}

export default Card;

Changes Made:

  1. ✅ Changed import from forwardRef to Ref type
  2. ✅ Added ref?: Ref<HTMLDivElement> to interface
  3. ✅ Removed forwardRef wrapper
  4. ✅ Added ref to props destructuring
  5. ✅ Removed displayName

Example 3: Input with useImperativeHandle

Before:

import { forwardRef, useRef, useImperativeHandle } from 'react';

const SearchInput = forwardRef((props, ref) => {
  const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    focus() {
      inputRef.current?.focus();
    },
    clear() {
      inputRef.current.value = '';
    },
    getValue() {
      return inputRef.current?.value || '';
    }
  }));

  return (
    <input
      ref={inputRef}
      type="text"
      placeholder="Search..."
      {...props}
    />
  );
});

export default SearchInput;

After:

import { useRef, useImperativeHandle } from 'react';

function SearchInput({ ref, ...props }) {
  const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    focus() {
      inputRef.current?.focus();
    },
    clear() {
      inputRef.current.value = '';
    },
    getValue() {
      return inputRef.current?.value || '';
    }
  }));

  return (
    <input
      ref={inputRef}
      type="text"
      placeholder="Search..."
      {...props}
    />
  );
}

export default SearchInput;

Usage (unchanged):

function SearchBar() {
  const searchRef = useRef();

  const handleClear = () => {
    searchRef.current?.clear();
  };

  return (
    <>
      <SearchInput ref={searchRef} />
      <button onClick={handleClear}>Clear</​button>
    </>
  );
}

Example 4: Component Library Pattern

Before:

import { forwardRef, ComponentPropsWithoutRef, ElementRef } from 'react';

type ButtonElement = ElementRef<'button'>;
type ButtonProps = ComponentPropsWithoutRef<'button'> & {
  variant?: 'primary' | 'secondary';
};

const Button = forwardRef<ButtonElement, ButtonProps>(
  ({ variant = 'primary', className, ...props }, ref) => {
    return (
      <button
        ref={ref}
        className={`btn btn-${variant} ${className || ''}`}
        {...props}
      />
    );
  }
);

Button.displayName = 'Button';

After:

import { Ref, ComponentPropsWithoutRef, ElementRef } from 'react';

type ButtonElement = ElementRef<'button'>;
type ButtonProps = ComponentPropsWithoutRef<'button'> & {
  variant?: 'primary' | 'secondary';
  ref?: Ref<ButtonElement>;
};

function Button({
  variant = 'primary',
  className,
  ref,
  ...props
}: ButtonProps) {
  return (
    <button
      ref={ref}
      className={`btn btn-${variant} ${className || ''}`}
      {...props}
    />
  );
}

</​examples>

<constraints> ## MUST
  • Add ref to props interface when using TypeScript
  • Use Ref<HTMLElement> type from React for TypeScript
  • Test that ref forwarding works after migration
  • Maintain component behavior exactly (only syntax changes)

SHOULD

  • Migrate components gradually (forwardRef still works)
  • Update tests to verify ref behavior
  • Use consistent prop ordering (ref near other element props)
  • Document breaking changes if part of public API

NEVER

  • Remove forwardRef if still on React 18
  • Change component behavior during migration
  • Break existing ref usage in parent components
  • Skip TypeScript type updates for ref prop

</​constraints>

<validation> ## After Migration
  1. Verify Ref Forwarding:

    const ref = useRef(null);
    <MyComponent ref={ref} />
    // ref.current should be the DOM element
    
  2. Check TypeScript Compilation:

    npx tsc --noEmit
    

    No errors about ref props

  3. Test Component Behavior:

    • Component renders correctly
    • Ref accesses correct DOM element
    • useImperativeHandle methods work (if used)
    • No console warnings about deprecated APIs
  4. Verify Backward Compatibility:

    • Existing usage still works
    • No breaking changes to component API
    • Tests pass

</​validation>


Migration Checklist

When migrating a component from forwardRef:

  • Remove forwardRef import
  • Remove forwardRef wrapper function
  • Add ref to props destructuring
  • Add ref type to TypeScript interface (if applicable)
  • Remove displayName if only used for forwardRef
  • Test ref forwarding works
  • Update component tests
  • Check TypeScript compilation
  • Verify no breaking changes to API

Common Migration Patterns

Pattern 1: Simple Ref Forwarding

// Before
const Comp = forwardRef((props, ref) => <div ref={ref} />);

// After
function Comp({ ref }) { return <div ref={ref} />; }

Pattern 2: With useImperativeHandle

// Before
const Comp = forwardRef((props, ref) => {
  useImperativeHandle(ref, () => ({ method() {} }));
  return <div />;
});

// After
function Comp({ ref }) {
  useImperativeHandle(ref, () => ({ method() {} }));
  return <div />;
}

Pattern 3: TypeScript with Generics

// Before
const Comp = forwardRef<HTMLDivElement, Props>((props, ref) => ...);

// After
function Comp({ ref, ...props }: Props & { ref?: Ref<HTMLDivElement> }) { ... }

For comprehensive forwardRef migration documentation, see: research/​react-19-comprehensive.md lines 978-1033.

Ref Cleanup Functions (New in React 19)

React 19 supports cleanup functions in ref callbacks:

<div
  ref={(node) => {
    console.log('Connected:', node);

    return () => {
      console.log('Disconnected:', node);
    };
  }}
/>

When Cleanup Runs:

  • Component unmounts
  • Ref changes to different element

This works with both ref-as-prop and the old forwardRef pattern.

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View