React Modernization
Master React version upgrades, class to hooks migration, concurrent features adoption, and codemods for automated transformation.
When to Use This Skill
-
Upgrading React applications to latest versions
-
Migrating class components to functional components with hooks
-
Adopting concurrent React features (Suspense, transitions)
-
Applying codemods for automated refactoring
-
Modernizing state management patterns
-
Updating to TypeScript
-
Improving performance with React 18+ features
Version Upgrade Path
React 16 → 17 → 18
Breaking Changes by Version:
React 17:
-
Event delegation changes
-
No event pooling
-
Effect cleanup timing
-
JSX transform (no React import needed)
React 18:
-
Automatic batching
-
Concurrent rendering
-
Strict Mode changes (double invocation)
-
New root API
-
Suspense on server
Class to Hooks Migration
State Management
// Before: Class component class Counter extends React.Component { constructor(props) { super(props); this.state = { count: 0, name: '' }; }
increment = () => { this.setState({ count: this.state.count + 1 }); }
render() { return ( <div> <p>Count: {this.state.count}</p> <button onClick={this.increment}>Increment</button> </div> ); } }
// After: Functional component with hooks function Counter() { const [count, setCount] = useState(0); const [name, setName] = useState('');
const increment = () => { setCount(count + 1); };
return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> </div> ); }
Lifecycle Methods to Hooks
// Before: Lifecycle methods class DataFetcher extends React.Component { state = { data: null, loading: true };
componentDidMount() { this.fetchData(); }
componentDidUpdate(prevProps) { if (prevProps.id !== this.props.id) { this.fetchData(); } }
componentWillUnmount() { this.cancelRequest(); }
fetchData = async () => {
const data = await fetch(/api/${this.props.id});
this.setState({ data, loading: false });
};
cancelRequest = () => { // Cleanup };
render() { if (this.state.loading) return <div>Loading...</div>; return <div>{this.state.data}</div>; } }
// After: useEffect hook function DataFetcher({ id }) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true);
useEffect(() => { let cancelled = false;
const fetchData = async () => {
try {
const response = await fetch(`/api/${id}`);
const result = await response.json();
if (!cancelled) {
setData(result);
setLoading(false);
}
} catch (error) {
if (!cancelled) {
console.error(error);
}
}
};
fetchData();
// Cleanup function
return () => {
cancelled = true;
};
}, [id]); // Re-run when id changes
if (loading) return <div>Loading...</div>; return <div>{data}</div>; }
Context and HOCs to Hooks
// Before: Context consumer and HOC const ThemeContext = React.createContext();
class ThemedButton extends React.Component { static contextType = ThemeContext;
render() { return ( <button style={{ background: this.context.theme }}> {this.props.children} </button> ); } }
// After: useContext hook function ThemedButton({ children }) { const { theme } = useContext(ThemeContext);
return ( <button style={{ background: theme }}> {children} </button> ); }
// Before: HOC for data fetching function withUser(Component) { return class extends React.Component { state = { user: null };
componentDidMount() {
fetchUser().then(user => this.setState({ user }));
}
render() {
return <Component {...this.props} user={this.state.user} />;
}
}; }
// After: Custom hook function useUser() { const [user, setUser] = useState(null);
useEffect(() => { fetchUser().then(setUser); }, []);
return user; }
function UserProfile() { const user = useUser(); if (!user) return <div>Loading...</div>; return <div>{user.name}</div>; }
React 18 Concurrent Features
New Root API
// 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 />);
Automatic Batching
// React 18: All updates are batched function handleClick() { setCount(c => c + 1); setFlag(f => !f); // Only one re-render (batched) }
// Even in async: setTimeout(() => { setCount(c => c + 1); setFlag(f => !f); // Still batched in React 18! }, 1000);
// Opt out if needed import { flushSync } from 'react-dom';
flushSync(() => { setCount(c => c + 1); }); // Re-render happens here setFlag(f => !f); // Another re-render
Transitions
import { useState, useTransition } from 'react';
function SearchResults() { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [isPending, startTransition] = useTransition();
const handleChange = (e) => { // Urgent: Update input immediately setQuery(e.target.value);
// Non-urgent: Update results (can be interrupted)
startTransition(() => {
setResults(searchResults(e.target.value));
});
};
return ( <> <input value={query} onChange={handleChange} /> {isPending && <Spinner />} <Results data={results} /> </> ); }
Suspense for Data Fetching
import { Suspense } from 'react';
// Resource-based data fetching (with React 18) const resource = fetchProfileData();
function ProfilePage() { return ( <Suspense fallback={<Loading />}> <ProfileDetails /> <Suspense fallback={<Loading />}> <ProfileTimeline /> </Suspense> </Suspense> ); }
function ProfileDetails() { // This will suspend if data not ready const user = resource.user.read(); return <h1>{user.name}</h1>; }
function ProfileTimeline() { const posts = resource.posts.read(); return <Timeline posts={posts} />; }
Codemods for Automation
Run React Codemods
Install jscodeshift
npm install -g jscodeshift
React 16.9 codemod (rename unsafe lifecycle methods)
npx react-codeshift <transform> <path>
Example: Rename UNSAFE_ methods
npx react-codeshift --parser=tsx
--transform=react-codeshift/transforms/rename-unsafe-lifecycles.js
src/
Update to new JSX Transform (React 17+)
npx react-codeshift --parser=tsx
--transform=react-codeshift/transforms/new-jsx-transform.js
src/
Class to Hooks (third-party)
npx codemod react/hooks/convert-class-to-function src/
Custom Codemod Example
// custom-codemod.js module.exports = function(file, api) { const j = api.jscodeshift; const root = j(file.source);
// Find setState calls root.find(j.CallExpression, { callee: { type: 'MemberExpression', property: { name: 'setState' } } }).forEach(path => { // Transform to useState // ... transformation logic });
return root.toSource(); };
// Run: jscodeshift -t custom-codemod.js src/
Performance Optimization
useMemo and useCallback
function ExpensiveComponent({ items, filter }) { // Memoize expensive calculation const filteredItems = useMemo(() => { return items.filter(item => item.category === filter); }, [items, filter]);
// Memoize callback to prevent child re-renders const handleClick = useCallback((id) => { console.log('Clicked:', id); }, []); // No dependencies, never changes
return ( <List items={filteredItems} onClick={handleClick} /> ); }
// Child component with memo const List = React.memo(({ items, onClick }) => { return items.map(item => ( <Item key={item.id} item={item} onClick={onClick} /> )); });
Code Splitting
import { lazy, Suspense } from 'react';
// Lazy load components const Dashboard = lazy(() => import('./Dashboard')); const Settings = lazy(() => import('./Settings'));
function App() { return ( <Suspense fallback={<Loading />}> <Routes> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/settings" element={<Settings />} /> </Routes> </Suspense> ); }
TypeScript Migration
// Before: JavaScript function Button({ onClick, children }) { return <button onClick={onClick}>{children}</button>; }
// After: TypeScript interface ButtonProps { onClick: () => void; children: React.ReactNode; }
function Button({ onClick, children }: ButtonProps) { return <button onClick={onClick}>{children}</button>; }
// 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
- Update dependencies incrementally (not all at once)
- Review breaking changes in release notes
- Set up testing suite
- Create feature branch
Class → Hooks Migration
- Identify class components to migrate
- Start with leaf components (no children)
- Convert state to useState
- Convert lifecycle to useEffect
- Convert context to useContext
- Extract custom hooks
- Test thoroughly
React 18 Upgrade
- Update to React 17 first (if needed)
- Update react and react-dom to 18
- Update @types/react if using TypeScript
- Change to createRoot API
- Test with StrictMode (double invocation)
- Address concurrent rendering issues
- Adopt Suspense/Transitions where beneficial
Performance
- Identify performance bottlenecks
- Add React.memo where appropriate
- Use useMemo/useCallback for expensive operations
- Implement code splitting
- Optimize re-renders
Testing
- Update test utilities (React Testing Library)
- Test with React 18 features
- Check for warnings in console
- Performance testing
Resources
-
references/breaking-changes.md: Version-specific breaking changes
-
references/codemods.md: Codemod usage guide
-
references/hooks-migration.md: Comprehensive hooks patterns
-
references/concurrent-features.md: React 18 concurrent features
-
assets/codemod-config.json: Codemod configurations
-
assets/migration-checklist.md: Step-by-step checklist
-
scripts/apply-codemods.sh: Automated codemod script
Best Practices
-
Incremental Migration: Don't migrate everything at once
-
Test Thoroughly: Comprehensive testing at each step
-
Use Codemods: Automate repetitive transformations
-
Start Simple: Begin with leaf components
-
Leverage StrictMode: Catch issues early
-
Monitor Performance: Measure before and after
-
Document Changes: Keep migration log
Common Pitfalls
-
Forgetting useEffect dependencies
-
Over-using useMemo/useCallback
-
Not handling cleanup in useEffect
-
Mixing class and functional patterns
-
Ignoring StrictMode warnings
-
Breaking change assumptions