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
Rename unsafe lifecycle methods
npx jscodeshift -t https://raw.githubusercontent.com/reactjs/react-codemod/master/transforms/rename-unsafe-lifecycles.js src/
Update React imports (React 17+)
npx jscodeshift -t https://raw.githubusercontent.com/reactjs/react-codemod/master/transforms/update-react-imports.js src/
Add error boundaries
npx jscodeshift -t https://raw.githubusercontent.com/reactjs/react-codemod/master/transforms/error-boundaries.js src/
For TypeScript files
npx jscodeshift -t https://raw.githubusercontent.com/reactjs/react-codemod/master/transforms/rename-unsafe-lifecycles.js --parser=tsx src/
Dry run to preview changes
npx jscodeshift -t https://raw.githubusercontent.com/reactjs/react-codemod/master/transforms/rename-unsafe-lifecycles.js --dry --print 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