React Modernization
by @wpank
Upgrade React apps by migrating class components to hooks, adopting React 18/19 concurrent features, running codemods, and adding TypeScript types.
clawhub install react-modernizationπ About This Skill
name: react-modernization model: reasoning
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:
WHEN
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 | not |
| 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 }) =>
{data})// 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
if (error) return
return
}
}// AFTER: Custom hook + functional component
function useUser(id: string) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(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
if (error) return
return
}
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
}
}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 ?
{user.name} : null
}
React 18+ Concurrent Features
New Root API (Required)
// BEFORE: React 17
import ReactDOM from 'react-dom'
ReactDOM.render( , document.getElementById('root'))// AFTER: React 18+
import { createRoot } from 'react-dom/client'
const root = createRoot(document.getElementById('root')!)
root.render( )
useTransition for Non-Urgent Updates
function SearchResults() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [isPending, startTransition] = useTransition() function handleChange(e: React.ChangeEvent) {
// Urgent: update input immediately
setQuery(e.target.value)
// Non-urgent: can be interrupted
startTransition(() => {
setResults(searchDatabase(e.target.value))
})
}
return (
<>
{isPending ? : }
>
)
}
Suspense for Data Fetching
// With React 19's use() hook
function ProfilePage({ userId }: { userId: string }) {
return (
}>
)
}function ProfileDetails({ userId }: { userId: string }) {
// use() suspends until promise resolves
const user = use(fetchUser(userId))
return
{user.name}
}
React 19: use() Hook
// Read promises directly in render
function Comments({ commentsPromise }) {
const comments = use(commentsPromise)
return comments.map(c => )
}// Read context (simpler than useContext)
function ThemeButton() {
const theme = use(ThemeContext)
return
}
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 (
)
}
Automated Codemods
Run Official React Codemods
# Update to new JSX transform (no React import needed)
npx codemod@latest react/19/replace-reactdom-renderUpdate deprecated APIs
npx codemod@latest react/19/replace-string-refClass 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 tsxFind deprecated lifecycle methods
rg "componentWillMount|componentWillReceiveProps|componentWillUpdate" --type tsxFind 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 (
)
}
// Type event handlers
function Form() {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
}
const handleChange = (e: React.ChangeEvent) => {
console.log(e.target.value)
}
return (
)
}// Generic components
interface ListProps {
items: T[]
renderItem: (item: T) => React.ReactNode
}
function List({ items, renderItem }: ListProps) {
return <>{items.map(renderItem)}>
}
Migration Checklist
Pre-Migration
Class β Hooks
useStateuseEffectReact 18+ Upgrade
createRoot APIPost-Migration
NEVER
// eslint-disable-next-line react-hooks/exhaustive-deps without understanding why