React Modernization
Guide for upgrading React applications, migrating patterns, and adopting modern features.
When to Use
-
Upgrading React to 18.x or 19.x
-
Migrating class components to function components with hooks
-
Adopting concurrent features (Suspense, Transitions, use())
-
Running codemods for automated transformations
-
Modernizing legacy patterns (HOCs to hooks, lifecycle to effects)
Upgrade Path
React 16/17 ──> React 18 ──> Adopt Concurrent Features ──> React 19 │ │ │ createRoot migration Suspense + Transitions use() + Actions Automatic batching useDeferredValue useOptimistic StrictMode changes Error Boundaries React Compiler
React 18 Migration
Breaking Changes
-
ReactDOM.render replaced by createRoot (required)
-
Automatic batching in all contexts (may change behavior)
-
Strict Mode double-renders effects in dev
Install & Codemod
npm install react@18 react-dom@18 @types/react@18 @types/react-dom@18 npx codemod react/19/replace-reactdom-render
// BEFORE (React 17) import ReactDOM from 'react-dom'; ReactDOM.render(<App />, document.getElementById('root'));
// AFTER (React 18+) import { createRoot } from 'react-dom/client'; createRoot(document.getElementById('root')!).render(<App />);
TypeScript Changes
-
React.FC no longer includes children — add explicitly to props
-
React.VFC removed — use React.FC
-
Stricter generics on useCallback / useMemo
Class to Function Component Migration
Priority Order
-
Leaf components (no children, simple props) — easiest wins
-
Container components (state + data fetching)
-
Higher-Order Components — extract to custom hooks
-
Ref-forwarding components — useRef + forwardRef
-
Error Boundaries — keep as class (no hook equivalent)
Lifecycle to Hooks Mapping
Class Lifecycle Hook Equivalent
constructor / state init useState(initialValue)
componentDidMount
useEffect(() => { ... }, [])
componentDidUpdate
useEffect(() => { ... }, [deps])
componentWillUnmount
useEffect return cleanup
shouldComponentUpdate
React.memo(Component)
getDerivedStateFromProps
Compute during render or useMemo
componentDidCatch
No hook — keep as class Error Boundary
Migration Example
// BEFORE: Class class UserProfile extends React.Component<Props, State> { state = { user: null, loading: true }; componentDidMount() { fetchUser(this.props.id).then(user => this.setState({ user, loading: false }) ); } componentDidUpdate(prev: Props) { if (prev.id !== this.props.id) { this.setState({ loading: true }); fetchUser(this.props.id).then(user => this.setState({ user, loading: false }) ); } } render() { if (this.state.loading) return <Spinner />; return <div>{this.state.user?.name}</div>; } }
// AFTER: Function + hooks function UserProfile({ id }: Props) { const [user, setUser] = useState<User | null>(null); const [loading, setLoading] = useState(true);
useEffect(() => { setLoading(true); fetchUser(id).then(u => { setUser(u); setLoading(false); }); }, [id]);
if (loading) return <Spinner />; return <div>{user?.name}</div>; }
HOC to Custom Hook
// BEFORE: HOC function withAuth(Component) { return (props) => { const user = useContext(AuthContext); if (!user) return <Redirect to="/login" />; return <Component {...props} user={user} />; }; }
// AFTER: Custom hook function useAuth() { const user = useContext(AuthContext); return { user, isAuthenticated: !!user }; }
Concurrent Features (React 18+)
Suspense Boundaries
<Suspense fallback={<Skeleton />}> <LazyComponent /> </Suspense>
useTransition — Non-Urgent Updates
const [isPending, startTransition] = useTransition();
function handleSearch(value: string) { setQuery(value); // Urgent: update input startTransition(() => setResults(filter(value))); // Deferred: filter }
useDeferredValue — Defer Expensive Renders
const deferredQuery = useDeferredValue(query); const results = useMemo(() => search(deferredQuery), [deferredQuery]);
React 19 Features
use() — Read Promises and Context
// Suspends until promise resolves function UserProfile({ userPromise }: { userPromise: Promise<User> }) { const user = use(userPromise); return <div>{user.name}</div>; }
// Conditional context reading (new in 19) function Theme({ show }: { show: boolean }) { if (!show) return null; const theme = use(ThemeContext); return <div style={{ color: theme.primary }}>Themed</div>; }
useActionState — Form Actions
const [state, formAction, isPending] = useActionState( async (prev, formData) => { const result = await login(formData); if (result.error) return { error: result.error }; redirect('/dashboard'); }, { error: null } );
return ( <form action={formAction}> <input name="email" type="email" /> <button disabled={isPending}> {isPending ? 'Signing in...' : 'Sign In'} </button> </form> );
useOptimistic — Optimistic UI
const [optimisticTodos, addOptimistic] = useOptimistic( todos, (state, newTodo: Todo) => [...state, newTodo] );
async function addTodo(formData: FormData) { const todo = { id: crypto.randomUUID(), text: formData.get('text') as string }; addOptimistic(todo); // Instant UI update await saveTodo(todo); // Server call }
Codemods
All React 19 codemods at once
npx codemod@latest react/19/migration-recipe
Individual codemods
npx codemod react/19/replace-reactdom-render # createRoot npx codemod react/19/replace-string-ref # string refs to useRef npx codemod react/19/replace-act-import # act() import path npx codemod react/19/replace-use-form-state # useFormState to useActionState
Migration Checklist
-
Update react, react-dom, and @types packages
-
Replace ReactDOM.render with createRoot
-
Run codemods for automated fixes
-
Fix TypeScript errors (FC children, etc.)
-
Test with StrictMode enabled (double-render effects)
-
Migrate class components (leaf first, then containers)
-
Replace HOCs with custom hooks
-
Add Suspense boundaries for code splitting
-
Adopt useTransition for non-urgent updates
-
Keep Error Boundaries as class components
-
Test all forms and user flows end-to-end