Agent skill
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 this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/migrating-from-forwardref
SKILL.md
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:
- Simpler API - Refs are just props, no special wrapper needed
- Better TypeScript - Easier type inference and typing
- Consistency - All props handled the same way
- Less Boilerplate - Fewer imports and wrapper functions
Migration Path:
forwardRefstill 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:
- ✅ Removed
forwardRefimport - ✅ Removed
forwardRefwrapper - ✅ Added
refto props destructuring - ✅ Removed unnecessary
displayName - ✅ 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:
- ✅ Changed import from
forwardReftoReftype - ✅ Added
ref?: Ref<HTMLDivElement>to interface - ✅ Removed
forwardRefwrapper - ✅ Added
refto props destructuring - ✅ 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
refto 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
forwardRefif still on React 18 - Change component behavior during migration
- Break existing ref usage in parent components
- Skip TypeScript type updates for ref prop
-
Verify Ref Forwarding:
javascriptconst ref = useRef(null); <MyComponent ref={ref} /> // ref.current should be the DOM element -
Check TypeScript Compilation:
bashnpx tsc --noEmitNo errors about ref props
-
Test Component Behavior:
- Component renders correctly
- Ref accesses correct DOM element
- useImperativeHandle methods work (if used)
- No console warnings about deprecated APIs
-
Verify Backward Compatibility:
- Existing usage still works
- No breaking changes to component API
- Tests pass
Migration Checklist
When migrating a component from forwardRef:
- Remove
forwardRefimport - Remove
forwardRefwrapper function - Add
refto props destructuring - Add
reftype to TypeScript interface (if applicable) - Remove
displayNameif 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.
Didn't find tool you were looking for?