Marketplace

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.

allowed_tools: Read, Write, Edit, Glob, Grep

$ Installer

git clone https://github.com/djankies/claude-configs /tmp/claude-configs && cp -r /tmp/claude-configs/react-19/skills/migrating-from-forwardref ~/.claude/skills/claude-configs

// tip: Run this command in your terminal to install the skill


name: migrating-from-forwardref description: 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. allowed-tools: Read, Write, Edit, Glob, Grep version: 1.0.0

Migrating from forwardRef to Ref as Prop

  • 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

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 }) { ... }

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>;
}

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>;
}

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.

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}
    />
  );
}
  • 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
  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

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.