595 lines
12 KiB
Markdown
595 lines
12 KiB
Markdown
---
|
|
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
|
|
|
|
<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:**
|
|
```javascript
|
|
// 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`:
|
|
```bash
|
|
# Use Grep tool
|
|
pattern: "forwardRef"
|
|
output_mode: "files_with_matches"
|
|
```
|
|
|
|
**Step 2: Understand Current Pattern**
|
|
|
|
Before (React 18):
|
|
```javascript
|
|
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):
|
|
```javascript
|
|
function MyButton({ children, className, ref }) {
|
|
return (
|
|
<button ref={ref} className={className}>
|
|
{children}
|
|
</button>
|
|
);
|
|
}
|
|
```
|
|
|
|
**Step 4: Update TypeScript Types (if applicable)**
|
|
|
|
Before:
|
|
```typescript
|
|
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:
|
|
```typescript
|
|
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:
|
|
```javascript
|
|
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:
|
|
```javascript
|
|
const FancyInput = forwardRef((props, ref) => {
|
|
const inputRef = useRef();
|
|
|
|
useImperativeHandle(ref, () => ({
|
|
focus: () => inputRef.current.focus(),
|
|
clear: () => { inputRef.current.value = ''; }
|
|
}));
|
|
|
|
return <input ref={inputRef} />;
|
|
});
|
|
```
|
|
|
|
After:
|
|
```javascript
|
|
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:**
|
|
|
|
```javascript
|
|
function ComplexComponent({ ref, innerRef, ...props }) {
|
|
return (
|
|
<div ref={ref}>
|
|
<input ref={innerRef} {...props} />
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
**If using generic components:**
|
|
|
|
```typescript
|
|
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):**
|
|
|
|
```javascript
|
|
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):**
|
|
|
|
```javascript
|
|
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:**
|
|
|
|
```typescript
|
|
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:**
|
|
|
|
```typescript
|
|
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:**
|
|
|
|
```javascript
|
|
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:**
|
|
|
|
```javascript
|
|
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):**
|
|
|
|
```javascript
|
|
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:**
|
|
|
|
```typescript
|
|
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:**
|
|
|
|
```typescript
|
|
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**:
|
|
```javascript
|
|
const ref = useRef(null);
|
|
<MyComponent ref={ref} />
|
|
// ref.current should be the DOM element
|
|
```
|
|
|
|
2. **Check TypeScript Compilation**:
|
|
```bash
|
|
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
|
|
```javascript
|
|
// Before
|
|
const Comp = forwardRef((props, ref) => <div ref={ref} />);
|
|
|
|
// After
|
|
function Comp({ ref }) { return <div ref={ref} />; }
|
|
```
|
|
|
|
### Pattern 2: With useImperativeHandle
|
|
```javascript
|
|
// 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
|
|
```typescript
|
|
// 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:
|
|
|
|
```javascript
|
|
<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.
|