Files
Saravanan Rajaraman 7f7b1b9b46 feat: Adds React 18 and 19 migration plugin (#1339)
- Adds React 18 and 19 migration orchestration plugins
- Introduces comprehensive upgrade toolkits for migrating legacy React 16/17 and 18 codebases to React 18.3.1 and 19, respectively. Each plugin bundles specialized agents and skills for exhaustive audit, dependency management, class/component API migration, test suite transformation, and batching regression fixes.
- The React 18 toolkit targets class-component-heavy apps, ensures safe lifecycle and context transitions, resolves dependency blockers, and fully automates test migrations including Enzyme removal. The React 19 toolkit addresses breaking changes such as removal of legacy APIs, defaultProps on function components, and forwardRef, while enforcing a gated, memory-resumable migration pipeline.
- Both plugins update documentation, plugin registries, and skill references to support reliable, repeatable enterprise-scale React migrations.
2026-04-09 15:18:52 +10:00

11 KiB

title
title
React 19 API Migrations Reference

React 19 API Migrations Reference

Complete before/after patterns for all React 19 breaking changes and removed APIs.


ReactDOM Root API Migration

React 19 requires createRoot() or hydrateRoot() for all apps. If the React 18 migration already ran, this is done. Verify it's correct.

Pattern 1: createRoot() CSR App

// Before (React 18 or earlier):
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));

// After (React 19):
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);

Pattern 2: hydrateRoot() SSR/Static App

// Before (React 18 server-rendered app):
import ReactDOM from 'react-dom';
ReactDOM.hydrate(<App />, document.getElementById('root'));

// After (React 19):
import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document.getElementById('root'), <App />);

Pattern 3: unmountComponentAtNode() Removed

// Before (React 18):
import ReactDOM from 'react-dom';
ReactDOM.unmountComponentAtNode(container);

// After (React 19):
const root = createRoot(container); // Save the root reference
// later:
root.unmount();

Caveat: If the root reference was never saved, you must refactor to pass it around or use a global registry.


findDOMNode() Removed

Pattern 1: Direct ref

// Before (React 18):
import { findDOMNode } from 'react-dom';
const domNode = findDOMNode(componentRef);

// After (React 19):
const domNode = componentRef.current; // refs point directly to DOM

Pattern 2: Class Component ref

// Before (React 18):
import { findDOMNode } from 'react-dom';
class MyComponent extends React.Component {
  render() {
    return <div ref={ref => this.node = ref}>Content</div>;
  }
  
  getWidth() {
    return findDOMNode(this).offsetWidth;
  }
}

// After (React 19):
// Note: findDOMNode() is removed in React 19. Eliminate the call entirely
// and use direct refs to access DOM nodes instead.
class MyComponent extends React.Component {
  nodeRef = React.createRef();
  
  render() {
    return <div ref={this.nodeRef}>Content</div>;
  }
  
  getWidth() {
    return this.nodeRef.current.offsetWidth;
  }
}

forwardRef() - Optional Modernization

Pattern 1: Function Component Direct ref

// Before (React 18):
import { forwardRef } from 'react';

const Input = forwardRef((props, ref) => (
  <input ref={ref} {...props} />
));

function App() {
  const inputRef = useRef(null);
  return <Input ref={inputRef} />;
}

// After (React 19):
// Simply accept ref as a regular prop:
function Input({ ref, ...props }) {
  return <input ref={ref} {...props} />;
}

function App() {
  const inputRef = useRef(null);
  return <Input ref={inputRef} />;
}

Pattern 2: forwardRef + useImperativeHandle

// Before (React 18):
import { forwardRef, useImperativeHandle } from 'react';

const TextInput = forwardRef((props, ref) => {
  const inputRef = useRef();
  
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
    clear: () => { inputRef.current.value = ''; }
  }));
  
  return <input ref={inputRef} {...props} />;
});

function App() {
  const textRef = useRef(null);
  return (
    <>
      <TextInput ref={textRef} />
      <button onClick={() => textRef.current.focus()}>Focus</button>
    </>
  );
}

// After (React 19):
function TextInput({ ref, ...props }) {
  const inputRef = useRef(null);
  
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
    clear: () => { inputRef.current.value = ''; }
  }));
  
  return <input ref={inputRef} {...props} />;
}

function App() {
  const textRef = useRef(null);
  return (
    <>
      <TextInput ref={textRef} />
      <button onClick={() => textRef.current.focus()}>Focus</button>
    </>
  );
}

Note: useImperativeHandle is still valid; only the forwardRef wrapper is removed.


defaultProps Removed

Pattern 1: Function Component with defaultProps

// Before (React 18):
function Button({ label = 'Click', disabled = false }) {
  return <button disabled={disabled}>{label}</button>;
}

// WORKS BUT is removed in React 19:
Button.defaultProps = {
  label: 'Click',
  disabled: false
};

// After (React 19):
// ES6 default params are now the ONLY way:
function Button({ label = 'Click', disabled = false }) {
  return <button disabled={disabled}>{label}</button>;
}

// Remove all defaultProps assignments

Pattern 2: Class Component defaultProps

// Before (React 18):
class Button extends React.Component {
  static defaultProps = {
    label: 'Click',
    disabled: false
  };
  
  render() {
    return <button disabled={this.props.disabled}>{this.props.label}</button>;
  }
}

// After (React 19):
// Use default params in constructor or class field:
class Button extends React.Component {
  constructor(props) {
    super(props);
    this.label = props.label || 'Click';
    this.disabled = props.disabled || false;
  }
  
  render() {
    return <button disabled={this.disabled}>{this.label}</button>;
  }
}

// Or simplify to function component with ES6 defaults:
function Button({ label = 'Click', disabled = false }) {
  return <button disabled={disabled}>{label}</button>;
}

Pattern 3: defaultProps with null

// Before (React 18):
function Component({ value }) {
  // defaultProps can set null to reset a parent-passed value
  return <div>{value}</div>;
}

Component.defaultProps = {
  value: null
};

// After (React 19):
// Use explicit null checks or nullish coalescing:
function Component({ value = null }) {
  return <div>{value}</div>;
}

// Or:
function Component({ value }) {
  return <div>{value ?? null}</div>;
}

useRef Without Initial Value

Pattern 1: useRef()

// Before (React 18):
const ref = useRef(); // undefined initially

// After (React 19):
// Explicitly pass null as initial value:
const ref = useRef(null);

// Then use current:
ref.current = someElement; // Set it manually later

Pattern 2: useRef with DOM Elements

// Before:
function Component() {
  const inputRef = useRef();
  return <input ref={inputRef} />;
}

// After:
function Component() {
  const inputRef = useRef(null); // Explicit null
  return <input ref={inputRef} />;
}

Legacy Context API Removed

Pattern 1: React.createContext vs contextTypes

// Before (React 18  not recommended but worked):
// Using contextTypes (old PropTypes-style context):
class MyComponent extends React.Component {
  static contextTypes = {
    theme: PropTypes.string
  };
  
  render() {
    return <div style={{ color: this.context.theme }}>Text</div>;
  }
}

// Provider using getChildContext (old API):
class App extends React.Component {
  static childContextTypes = {
    theme: PropTypes.string
  };
  
  getChildContext() {
    return { theme: 'dark' };
  }
  
  render() {
    return <MyComponent />;
  }
}

// After (React 19):
// Use createContext (modern API):
const ThemeContext = React.createContext(null);

function MyComponent() {
  const theme = useContext(ThemeContext);
  return <div style={{ color: theme }}>Text</div>;
}

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <MyComponent />
    </ThemeContext.Provider>
  );
}

Pattern 2: Class Component Consuming createContext

// Before (class component consuming old context):
class MyComponent extends React.Component {
  static contextType = ThemeContext;
  
  render() {
    return <div style={{ color: this.context }}>Text</div>;
  }
}

// After (still works in React 19):
// No change needed for static contextType
// Continue using this.context

Important: If you're still using the old contextTypes + getChildContext pattern (not modern createContext), you must migrate to createContext the old pattern is completely removed.


String Refs Removed

Pattern 1: this.refs String Refs

// Before (React 18):
class Component extends React.Component {
  render() {
    return (
      <>
        <input ref="inputRef" />
        <button onClick={() => this.refs.inputRef.focus()}>Focus</button>
      </>
    );
  }
}

// After (React 19):
class Component extends React.Component {
  inputRef = React.createRef();
  
  render() {
    return (
      <>
        <input ref={this.inputRef} />
        <button onClick={() => this.inputRef.current.focus()}>Focus</button>
      </>
    );
  }
}
// Before (React 18):
class Component extends React.Component {
  render() {
    return (
      <>
        <input ref="inputRef" />
        <button onClick={() => this.refs.inputRef.focus()}>Focus</button>
      </>
    );
  }
}

// After (React 19  callback is more flexible):
class Component extends React.Component {
  constructor(props) {
    super(props);
    this.inputRef = null;
  }
  
  render() {
    return (
      <>
        <input ref={(el) => { this.inputRef = el; }} />
        <button onClick={() => this.inputRef?.focus()}>Focus</button>
      </>
    );
  }
}

Unused React Import Removal

Pattern 1: React Import After JSX Transform

// Before (React 18):
import React from 'react'; // Needed for JSX transform

function Component() {
  return <div>Text</div>;
}

// After (React 19 with new JSX transform):
// Remove the React import if it's not used:
function Component() {
  return <div>Text</div>;
}

// BUT keep it if you use React.* APIs:
import React from 'react';

function Component() {
  return <div>{React.useState ? 'yes' : 'no'}</div>;
}

Scan for Unused React Imports

# Find imports that can be removed:
grep -rn "^import React from 'react';" src/ --include="*.js" --include="*.jsx"
# Then check if the file uses React.*, useContext, etc.

Complete Migration Checklist

# 1. Find all ReactDOM.render calls:
grep -rn "ReactDOM.render" src/ --include="*.js" --include="*.jsx"
# Should be converted to createRoot

# 2. Find all ReactDOM.hydrate calls:
grep -rn "ReactDOM.hydrate" src/ --include="*.js" --include="*.jsx"
# Should be converted to hydrateRoot

# 3. Find all forwardRef usages:
grep -rn "forwardRef" src/ --include="*.js" --include="*.jsx"
# Check each one to see if it can be removed (most can)

# 4. Find all .defaultProps assignments:
grep -rn "\.defaultProps\s*=" src/ --include="*.js" --include="*.jsx"
# Replace with ES6 default params

# 5. Find all useRef() without initial value:
grep -rn "useRef()" src/ --include="*.js" --include="*.jsx"
# Add null: useRef(null)

# 6. Find old context (contextTypes):
grep -rn "contextTypes\|childContextTypes\|getChildContext" src/ --include="*.js" --include="*.jsx"
# Migrate to createContext

# 7. Find string refs (ref="name"):
grep -rn 'ref="' src/ --include="*.js" --include="*.jsx"
# Migrate to createRef or callback ref

# 8. Find unused React imports:
grep -rn "^import React from 'react';" src/ --include="*.js" --include="*.jsx"
# Check if React is used in the file