Claude-skill-registry-data 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.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry-data
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry-data "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/migrating-from-forwardref" ~/.claude/skills/majiayu000-claude-skill-registry-data-migrating-from-forwardref && rm -rf "$T"
manifest: data/migrating-from-forwardref/SKILL.md
source content

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.