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
javascript// 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
javascript// 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
javascript// 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
javascript// 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
javascript// 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
javascriptimport { 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
javascriptimport { 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
bash# 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
javascript// 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
javascriptfunction 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
javascriptimport { 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
typescript// 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
markdown### 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

