React Modernization
Upgrade React applications from class components to hooks, adopt concurrent features, and migrate between major versions.
WHAT
Systematic patterns for modernizing React codebases:
- Class-to-hooks migration with lifecycle method mappings
- React 18/19 concurrent features adoption
- TypeScript migration for React components
- Automated codemods for bulk refactoring
- Performance optimization with modern APIs
WHEN
- Migrating class components to functional components with hooks
- Upgrading React 16/17 apps to React 18/19
- Adopting concurrent features (Suspense, transitions, use)
- Converting HOCs and render props to custom hooks
- Adding TypeScript to React projects
KEYWORDS
react upgrade, class to hooks, useEffect, useState, react 18, react 19, concurrent, suspense, transition, codemod, migrate, modernize, functional component
Installation
OpenClaw / Moltbot / Clawbot
npx clawhub@latest install react-modernization
Version Upgrade Paths
React 17 → 18 Breaking Changes
| Change | Impact | Migration |
|---|---|---|
| New root API | Required | ReactDOM.render → createRoot |
| Automatic batching | Behavior | State updates batch in async code now |
| Strict Mode | Dev only | Effects fire twice (mount/unmount/mount) |
| Suspense on server | Optional | Enable SSR streaming |
React 18 → 19 Breaking Changes
| Change | Impact | Migration |
|---|---|---|
use() hook | New API | Read promises/context in render |
ref as prop | Simplified | No more forwardRef needed |
| Context as provider | Simplified | <Context> not <Context.Provider> |
| Async actions | New pattern | useActionState, useOptimistic |
Class to Hooks Migration
Lifecycle Method Mappings
// componentDidMount → useEffect with empty deps
useEffect(() => {
fetchData()
}, [])
// componentDidUpdate → useEffect with deps
useEffect(() => {
updateWhenIdChanges()
}, [id])
// componentWillUnmount → useEffect cleanup
useEffect(() => {
const subscription = subscribe()
return () => subscription.unsubscribe()
}, [])
// shouldComponentUpdate → React.memo
const Component = React.memo(({ data }) => <div>{data}</div>)
// getDerivedStateFromProps → useMemo
const derivedValue = useMemo(() => computeFrom(props), [props])
State Migration Pattern
// BEFORE: Class with multiple state properties
class UserProfile extends React.Component {
state = { user: null, loading: true, error: null }
componentDidMount() {
fetchUser(this.props.id)
.then(user => this.setState({ user, loading: false }))
.catch(error => this.setState({ error, loading: false }))
}
componentDidUpdate(prevProps) {
if (prevProps.id !== this.props.id) {
this.setState({ loading: true })
fetchUser(this.props.id)
.then(user => this.setState({ user, loading: false }))
}
}
render() {
const { user, loading, error } = this.state
if (loading) return <Spinner />
if (error) return <Error message={error.message} />
return <Profile user={user} />
}
}
// AFTER: Custom hook + functional component
function useUser(id: string) {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
let cancelled = false
setLoading(true)
fetchUser(id)
.then(data => {
if (!cancelled) {
setUser(data)
setLoading(false)
}
})
.catch(err => {
if (!cancelled) {
setError(err)
setLoading(false)
}
})
return () => { cancelled = true }
}, [id])
return { user, loading, error }
}
function UserProfile({ id }: { id: string }) {
const { user, loading, error } = useUser(id)
if (loading) return <Spinner />
if (error) return <Error message={error.message} />
return <Profile user={user} />
}
HOC to Hook Migration
// BEFORE: Higher-Order Component
function withUser(Component) {
return function WithUser(props) {
const [user, setUser] = useState(null)
useEffect(() => { fetchUser().then(setUser) }, [])
return <Component {...props} user={user} />
}
}
const ProfileWithUser = withUser(Profile)
// AFTER: Custom hook (simpler, composable)
function useCurrentUser() {
const [user, setUser] = useState(null)
useEffect(() => { fetchUser().then(setUser) }, [])
return user
}
function Profile() {
const user = useCurrentUser()
return user ? <div>{user.name}</div> : null
}
React 18+ Concurrent Features
New Root API (Required)
// BEFORE: React 17
import ReactDOM from 'react-dom'
ReactDOM.render(<App />, document.getElementById('root'))
// AFTER: React 18+
import { createRoot } from 'react-dom/client'
const root = createRoot(document.getElementById('root')!)
root.render(<App />)
useTransition for Non-Urgent Updates
function SearchResults() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [isPending, startTransition] = useTransition()
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
// Urgent: update input immediately
setQuery(e.target.value)
// Non-urgent: can be interrupted
startTransition(() => {
setResults(searchDatabase(e.target.value))
})
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending ? <Spinner /> : <ResultsList data={results} />}
</>
)
}
Suspense for Data Fetching
// With React 19's use() hook
function ProfilePage({ userId }: { userId: string }) {
return (
<Suspense fallback={<ProfileSkeleton />}>
<ProfileDetails userId={userId} />
</Suspense>
)
}
function ProfileDetails({ userId }: { userId: string }) {
// use() suspends until promise resolves
const user = use(fetchUser(userId))
return <h1>{user.name}</h1>
}
React 19: use() Hook
// Read promises directly in render
function Comments({ commentsPromise }) {
const comments = use(commentsPromise)
return comments.map(c => <Comment key={c.id} {...c} />)
}
// Read context (simpler than useContext)
function ThemeButton() {
const theme = use(ThemeContext)
return <button className={theme}>Click</button>
}
React 19: Actions
// useActionState for form submissions
function UpdateName() {
const [error, submitAction, isPending] = useActionState(
async (previousState, formData) => {
const error = await updateName(formData.get('name'))
if (error) return error
redirect('/profile')
},
null
)
return (
<form action={submitAction}>
<input name="name" />
<button disabled={isPending}>Update</button>
{error && <p>{error}</p>}
</form>
)
}
Automated Codemods
Run Official React Codemods
# Update to new JSX transform (no React import needed)
npx codemod@latest react/19/replace-reactdom-render
# Update deprecated APIs
npx codemod@latest react/19/replace-string-ref
# Class to function components
npx codemod@latest react/19/replace-use-form-state
Manual Search Patterns
# Find class components
rg "class \w+ extends (React\.)?Component" --type tsx
# Find deprecated lifecycle methods
rg "componentWillMount|componentWillReceiveProps|componentWillUpdate" --type tsx
# Find ReactDOM.render (needs migration to createRoot)
rg "ReactDOM\.render" --type tsx
TypeScript Migration
// Add types to functional components
interface ButtonProps {
onClick: () => void
children: React.ReactNode
variant?: 'primary' | 'secondary'
}
function Button({ onClick, children, variant = 'primary' }: ButtonProps) {
return (
<button onClick={onClick} className={variant}>
{children}
</button>
)
}
// Type event handlers
function Form() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value)
}
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} />
</form>
)
}
// Generic components
interface ListProps<T> {
items: T[]
renderItem: (item: T) => React.ReactNode
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <>{items.map(renderItem)}</>
}
Migration Checklist
Pre-Migration
- Upgrade dependencies incrementally
- Review breaking changes in release notes
- Set up comprehensive test coverage
- Create feature branch
Class → Hooks
- Start with leaf components (no children)
- Convert state to
useState - Convert lifecycle to
useEffect - Extract shared logic to custom hooks
- Convert HOCs to hooks where possible
React 18+ Upgrade
- Update to
createRootAPI - Test with StrictMode double-invocation
- Address hydration mismatches
- Adopt Suspense boundaries where beneficial
- Use transitions for expensive updates
Post-Migration
- Run full test suite
- Check for console warnings
- Profile performance before/after
- Document changes for team
NEVER
- Skip testing after migration
- Migrate multiple components in one commit
- Ignore StrictMode warnings (they reveal bugs)
- Use
// eslint-disable-next-line react-hooks/exhaustive-depswithout understanding why - Mix class and hooks in same component