12 KiB
name, description, allowed-tools, version
| name | description | allowed-tools | version |
|---|---|---|---|
| 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. | Read, Write, Edit, Glob, Grep | 1.0.0 |
Migrating from forwardRef to Ref as Prop
This skill teaches you how to migrate from the deprecated `forwardRef` API to React 19's ref-as-prop pattern. 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
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.
## Example 1: Simple Button MigrationBefore (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:
const ref = useRef(null); <MyComponent ref={ref} /> // ref.current should be the DOM element -
Check TypeScript Compilation:
npx 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.