513 lines
11 KiB
Markdown
513 lines
11 KiB
Markdown
# React Modernization Implementation Playbook
|
|
|
|
This file contains detailed patterns, checklists, and code samples referenced by the skill.
|
|
|
|
# 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
|
|
```javascript
|
|
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
|
|
```javascript
|
|
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
|
|
```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
|
|
```javascript
|
|
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
|
|
```javascript
|
|
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
|
|
|
|
```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
|
|
|
|
1. **Incremental Migration**: Don't migrate everything at once
|
|
2. **Test Thoroughly**: Comprehensive testing at each step
|
|
3. **Use Codemods**: Automate repetitive transformations
|
|
4. **Start Simple**: Begin with leaf components
|
|
5. **Leverage StrictMode**: Catch issues early
|
|
6. **Monitor Performance**: Measure before and after
|
|
7. **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
|