style: format all files with prettier

This commit is contained in:
Seth Hobson
2026-01-19 17:07:03 -05:00
parent 8d37048deb
commit 56848874a2
355 changed files with 15215 additions and 10241 deletions

View File

@@ -20,18 +20,21 @@ Master AngularJS to Angular migration, including hybrid apps, component conversi
## Migration Strategies
### 1. Big Bang (Complete Rewrite)
- Rewrite entire app in Angular
- Parallel development
- Switch over at once
- **Best for:** Small apps, green field projects
### 2. Incremental (Hybrid Approach)
- Run AngularJS and Angular side-by-side
- Migrate feature by feature
- ngUpgrade for interop
- **Best for:** Large apps, continuous delivery
### 3. Vertical Slice
- Migrate one feature completely
- New features in Angular, maintain old in AngularJS
- Gradually replace
@@ -41,30 +44,27 @@ Master AngularJS to Angular migration, including hybrid apps, component conversi
```typescript
// main.ts - Bootstrap hybrid app
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { UpgradeModule } from '@angular/upgrade/static';
import { AppModule } from './app/app.module';
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { UpgradeModule } from "@angular/upgrade/static";
import { AppModule } from "./app/app.module";
platformBrowserDynamic()
.bootstrapModule(AppModule)
.then(platformRef => {
.then((platformRef) => {
const upgrade = platformRef.injector.get(UpgradeModule);
// Bootstrap AngularJS
upgrade.bootstrap(document.body, ['myAngularJSApp'], { strictDi: true });
upgrade.bootstrap(document.body, ["myAngularJSApp"], { strictDi: true });
});
```
```typescript
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { UpgradeModule } from '@angular/upgrade/static';
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { UpgradeModule } from "@angular/upgrade/static";
@NgModule({
imports: [
BrowserModule,
UpgradeModule
]
imports: [BrowserModule, UpgradeModule],
})
export class AppModule {
constructor(private upgrade: UpgradeModule) {}
@@ -78,36 +78,39 @@ export class AppModule {
## Component Migration
### AngularJS Controller → Angular Component
```javascript
// Before: AngularJS controller
angular.module('myApp').controller('UserController', function($scope, UserService) {
$scope.user = {};
angular
.module("myApp")
.controller("UserController", function ($scope, UserService) {
$scope.user = {};
$scope.loadUser = function(id) {
UserService.getUser(id).then(function(user) {
$scope.user = user;
});
};
$scope.loadUser = function (id) {
UserService.getUser(id).then(function (user) {
$scope.user = user;
});
};
$scope.saveUser = function() {
UserService.saveUser($scope.user);
};
});
$scope.saveUser = function () {
UserService.saveUser($scope.user);
};
});
```
```typescript
// After: Angular component
import { Component, OnInit } from '@angular/core';
import { UserService } from './user.service';
import { Component, OnInit } from "@angular/core";
import { UserService } from "./user.service";
@Component({
selector: 'app-user',
selector: "app-user",
template: `
<div>
<h2>{{ user.name }}</h2>
<button (click)="saveUser()">Save</button>
</div>
`
`,
})
export class UserComponent implements OnInit {
user: any = {};
@@ -119,7 +122,7 @@ export class UserComponent implements OnInit {
}
loadUser(id: number) {
this.userService.getUser(id).subscribe(user => {
this.userService.getUser(id).subscribe((user) => {
this.user = user;
});
}
@@ -131,37 +134,38 @@ export class UserComponent implements OnInit {
```
### AngularJS Directive → Angular Component
```javascript
// Before: AngularJS directive
angular.module('myApp').directive('userCard', function() {
angular.module("myApp").directive("userCard", function () {
return {
restrict: 'E',
restrict: "E",
scope: {
user: '=',
onDelete: '&'
user: "=",
onDelete: "&",
},
template: `
<div class="card">
<h3>{{ user.name }}</h3>
<button ng-click="onDelete()">Delete</button>
</div>
`
`,
};
});
```
```typescript
// After: Angular component
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Component, Input, Output, EventEmitter } from "@angular/core";
@Component({
selector: 'app-user-card',
selector: "app-user-card",
template: `
<div class="card">
<h3>{{ user.name }}</h3>
<button (click)="delete.emit()">Delete</button>
</div>
`
`,
})
export class UserCardComponent {
@Input() user: any;
@@ -175,26 +179,26 @@ export class UserCardComponent {
```javascript
// Before: AngularJS service
angular.module('myApp').factory('UserService', function($http) {
angular.module("myApp").factory("UserService", function ($http) {
return {
getUser: function(id) {
return $http.get('/api/users/' + id);
getUser: function (id) {
return $http.get("/api/users/" + id);
},
saveUser: function (user) {
return $http.post("/api/users", user);
},
saveUser: function(user) {
return $http.post('/api/users', user);
}
};
});
```
```typescript
// After: Angular service
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";
@Injectable({
providedIn: 'root'
providedIn: "root",
})
export class UserService {
constructor(private http: HttpClient) {}
@@ -204,7 +208,7 @@ export class UserService {
}
saveUser(user: any): Observable<any> {
return this.http.post('/api/users', user);
return this.http.post("/api/users", user);
}
}
```
@@ -212,30 +216,31 @@ export class UserService {
## Dependency Injection Changes
### Downgrading Angular → AngularJS
```typescript
// Angular service
import { Injectable } from '@angular/core';
import { Injectable } from "@angular/core";
@Injectable({ providedIn: 'root' })
@Injectable({ providedIn: "root" })
export class NewService {
getData() {
return 'data from Angular';
return "data from Angular";
}
}
// Make available to AngularJS
import { downgradeInjectable } from '@angular/upgrade/static';
import { downgradeInjectable } from "@angular/upgrade/static";
angular.module('myApp')
.factory('newService', downgradeInjectable(NewService));
angular.module("myApp").factory("newService", downgradeInjectable(NewService));
// Use in AngularJS
angular.module('myApp').controller('OldController', function(newService) {
angular.module("myApp").controller("OldController", function (newService) {
console.log(newService.getData());
});
```
### Upgrading AngularJS → Angular
```typescript
// AngularJS service
angular.module('myApp').factory('oldService', function() {
@@ -274,30 +279,30 @@ export class NewComponent {
```javascript
// Before: AngularJS routing
angular.module('myApp').config(function($routeProvider) {
angular.module("myApp").config(function ($routeProvider) {
$routeProvider
.when('/users', {
template: '<user-list></user-list>'
.when("/users", {
template: "<user-list></user-list>",
})
.when('/users/:id', {
template: '<user-detail></user-detail>'
.when("/users/:id", {
template: "<user-detail></user-detail>",
});
});
```
```typescript
// After: Angular routing
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
const routes: Routes = [
{ path: 'users', component: UserListComponent },
{ path: 'users/:id', component: UserDetailComponent }
{ path: "users", component: UserListComponent },
{ path: "users/:id", component: UserDetailComponent },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
exports: [RouterModule],
})
export class AppRoutingModule {}
```
@@ -307,8 +312,8 @@ export class AppRoutingModule {}
```html
<!-- Before: AngularJS -->
<form name="userForm" ng-submit="saveUser()">
<input type="text" ng-model="user.name" required>
<input type="email" ng-model="user.email" required>
<input type="text" ng-model="user.name" required />
<input type="email" ng-model="user.email" required />
<button ng-disabled="userForm.$invalid">Save</button>
</form>
```

View File

@@ -20,29 +20,30 @@ Master database schema and data migrations across ORMs (Sequelize, TypeORM, Pris
## ORM Migrations
### Sequelize Migrations
```javascript
// migrations/20231201-create-users.js
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('users', {
await queryInterface.createTable("users", {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
autoIncrement: true,
},
email: {
type: Sequelize.STRING,
unique: true,
allowNull: false
allowNull: false,
},
createdAt: Sequelize.DATE,
updatedAt: Sequelize.DATE
updatedAt: Sequelize.DATE,
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('users');
}
await queryInterface.dropTable("users");
},
};
// Run: npx sequelize-cli db:migrate
@@ -50,40 +51,41 @@ module.exports = {
```
### TypeORM Migrations
```typescript
// migrations/1701234567-CreateUsers.ts
import { MigrationInterface, QueryRunner, Table } from 'typeorm';
import { MigrationInterface, QueryRunner, Table } from "typeorm";
export class CreateUsers1701234567 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'users',
name: "users",
columns: [
{
name: 'id',
type: 'int',
name: "id",
type: "int",
isPrimary: true,
isGenerated: true,
generationStrategy: 'increment'
generationStrategy: "increment",
},
{
name: 'email',
type: 'varchar',
isUnique: true
name: "email",
type: "varchar",
isUnique: true,
},
{
name: 'created_at',
type: 'timestamp',
default: 'CURRENT_TIMESTAMP'
}
]
})
name: "created_at",
type: "timestamp",
default: "CURRENT_TIMESTAMP",
},
],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('users');
await queryRunner.dropTable("users");
}
}
@@ -92,6 +94,7 @@ export class CreateUsers1701234567 implements MigrationInterface {
```
### Prisma Migrations
```prisma
// schema.prisma
model User {
@@ -107,41 +110,41 @@ model User {
## Schema Transformations
### Adding Columns with Defaults
```javascript
// Safe migration: add column with default
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('users', 'status', {
await queryInterface.addColumn("users", "status", {
type: Sequelize.STRING,
defaultValue: 'active',
allowNull: false
defaultValue: "active",
allowNull: false,
});
},
down: async (queryInterface) => {
await queryInterface.removeColumn('users', 'status');
}
await queryInterface.removeColumn("users", "status");
},
};
```
### Renaming Columns (Zero Downtime)
```javascript
// Step 1: Add new column
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('users', 'full_name', {
type: Sequelize.STRING
await queryInterface.addColumn("users", "full_name", {
type: Sequelize.STRING,
});
// Copy data from old column
await queryInterface.sequelize.query(
'UPDATE users SET full_name = name'
);
await queryInterface.sequelize.query("UPDATE users SET full_name = name");
},
down: async (queryInterface) => {
await queryInterface.removeColumn('users', 'full_name');
}
await queryInterface.removeColumn("users", "full_name");
},
};
// Step 2: Update application to use new column
@@ -149,26 +152,27 @@ module.exports = {
// Step 3: Remove old column
module.exports = {
up: async (queryInterface) => {
await queryInterface.removeColumn('users', 'name');
await queryInterface.removeColumn("users", "name");
},
down: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('users', 'name', {
type: Sequelize.STRING
await queryInterface.addColumn("users", "name", {
type: Sequelize.STRING,
});
}
},
};
```
### Changing Column Types
```javascript
module.exports = {
up: async (queryInterface, Sequelize) => {
// For large tables, use multi-step approach
// 1. Add new column
await queryInterface.addColumn('users', 'age_new', {
type: Sequelize.INTEGER
await queryInterface.addColumn("users", "age_new", {
type: Sequelize.INTEGER,
});
// 2. Copy and transform data
@@ -179,34 +183,35 @@ module.exports = {
`);
// 3. Drop old column
await queryInterface.removeColumn('users', 'age');
await queryInterface.removeColumn("users", "age");
// 4. Rename new column
await queryInterface.renameColumn('users', 'age_new', 'age');
await queryInterface.renameColumn("users", "age_new", "age");
},
down: async (queryInterface, Sequelize) => {
await queryInterface.changeColumn('users', 'age', {
type: Sequelize.STRING
await queryInterface.changeColumn("users", "age", {
type: Sequelize.STRING,
});
}
},
};
```
## Data Transformations
### Complex Data Migration
```javascript
module.exports = {
up: async (queryInterface, Sequelize) => {
// Get all records
const [users] = await queryInterface.sequelize.query(
'SELECT id, address_string FROM users'
"SELECT id, address_string FROM users",
);
// Transform each record
for (const user of users) {
const addressParts = user.address_string.split(',');
const addressParts = user.address_string.split(",");
await queryInterface.sequelize.query(
`UPDATE users
@@ -219,20 +224,20 @@ module.exports = {
id: user.id,
street: addressParts[0]?.trim(),
city: addressParts[1]?.trim(),
state: addressParts[2]?.trim()
}
}
state: addressParts[2]?.trim(),
},
},
);
}
// Drop old column
await queryInterface.removeColumn('users', 'address_string');
await queryInterface.removeColumn("users", "address_string");
},
down: async (queryInterface, Sequelize) => {
// Reconstruct original column
await queryInterface.addColumn('users', 'address_string', {
type: Sequelize.STRING
await queryInterface.addColumn("users", "address_string", {
type: Sequelize.STRING,
});
await queryInterface.sequelize.query(`
@@ -240,16 +245,17 @@ module.exports = {
SET address_string = CONCAT(street, ', ', city, ', ', state)
`);
await queryInterface.removeColumn('users', 'street');
await queryInterface.removeColumn('users', 'city');
await queryInterface.removeColumn('users', 'state');
}
await queryInterface.removeColumn("users", "street");
await queryInterface.removeColumn("users", "city");
await queryInterface.removeColumn("users", "state");
},
};
```
## Rollback Strategies
### Transaction-Based Migrations
```javascript
module.exports = {
up: async (queryInterface, Sequelize) => {
@@ -257,15 +263,15 @@ module.exports = {
try {
await queryInterface.addColumn(
'users',
'verified',
"users",
"verified",
{ type: Sequelize.BOOLEAN, defaultValue: false },
{ transaction }
{ transaction },
);
await queryInterface.sequelize.query(
'UPDATE users SET verified = true WHERE email_verified_at IS NOT NULL',
{ transaction }
"UPDATE users SET verified = true WHERE email_verified_at IS NOT NULL",
{ transaction },
);
await transaction.commit();
@@ -276,62 +282,64 @@ module.exports = {
},
down: async (queryInterface) => {
await queryInterface.removeColumn('users', 'verified');
}
await queryInterface.removeColumn("users", "verified");
},
};
```
### Checkpoint-Based Rollback
```javascript
module.exports = {
up: async (queryInterface, Sequelize) => {
// Create backup table
await queryInterface.sequelize.query(
'CREATE TABLE users_backup AS SELECT * FROM users'
"CREATE TABLE users_backup AS SELECT * FROM users",
);
try {
// Perform migration
await queryInterface.addColumn('users', 'new_field', {
type: Sequelize.STRING
await queryInterface.addColumn("users", "new_field", {
type: Sequelize.STRING,
});
// Verify migration
const [result] = await queryInterface.sequelize.query(
"SELECT COUNT(*) as count FROM users WHERE new_field IS NULL"
"SELECT COUNT(*) as count FROM users WHERE new_field IS NULL",
);
if (result[0].count > 0) {
throw new Error('Migration verification failed');
throw new Error("Migration verification failed");
}
// Drop backup
await queryInterface.dropTable('users_backup');
await queryInterface.dropTable("users_backup");
} catch (error) {
// Restore from backup
await queryInterface.sequelize.query('DROP TABLE users');
await queryInterface.sequelize.query("DROP TABLE users");
await queryInterface.sequelize.query(
'CREATE TABLE users AS SELECT * FROM users_backup'
"CREATE TABLE users AS SELECT * FROM users_backup",
);
await queryInterface.dropTable('users_backup');
await queryInterface.dropTable("users_backup");
throw error;
}
}
},
};
```
## Zero-Downtime Migrations
### Blue-Green Deployment Strategy
```javascript
// Phase 1: Make changes backward compatible
module.exports = {
up: async (queryInterface, Sequelize) => {
// Add new column (both old and new code can work)
await queryInterface.addColumn('users', 'email_new', {
type: Sequelize.STRING
await queryInterface.addColumn("users", "email_new", {
type: Sequelize.STRING,
});
}
},
};
// Phase 2: Deploy code that writes to both columns
@@ -344,7 +352,7 @@ module.exports = {
SET email_new = email
WHERE email_new IS NULL
`);
}
},
};
// Phase 4: Deploy code that reads from new column
@@ -352,44 +360,45 @@ module.exports = {
// Phase 5: Remove old column
module.exports = {
up: async (queryInterface) => {
await queryInterface.removeColumn('users', 'email');
}
await queryInterface.removeColumn("users", "email");
},
};
```
## Cross-Database Migrations
### PostgreSQL to MySQL
```javascript
// Handle differences
module.exports = {
up: async (queryInterface, Sequelize) => {
const dialectName = queryInterface.sequelize.getDialect();
if (dialectName === 'mysql') {
await queryInterface.createTable('users', {
if (dialectName === "mysql") {
await queryInterface.createTable("users", {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
autoIncrement: true,
},
data: {
type: Sequelize.JSON // MySQL JSON type
}
type: Sequelize.JSON, // MySQL JSON type
},
});
} else if (dialectName === 'postgres') {
await queryInterface.createTable('users', {
} else if (dialectName === "postgres") {
await queryInterface.createTable("users", {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
autoIncrement: true,
},
data: {
type: Sequelize.JSONB // PostgreSQL JSONB type
}
type: Sequelize.JSONB, // PostgreSQL JSONB type
},
});
}
}
},
};
```

View File

@@ -34,6 +34,7 @@ PATCH: Bug fixes, backward compatible
## Dependency Analysis
### Audit Dependencies
```bash
# npm
npm outdated
@@ -50,6 +51,7 @@ npx npm-check-updates -u # Update package.json
```
### Analyze Dependency Tree
```bash
# See why a package is installed
npm ls package-name
@@ -68,23 +70,23 @@ npx madge --image graph.png src/
```javascript
// compatibility-matrix.js
const compatibilityMatrix = {
'react': {
'16.x': {
'react-dom': '^16.0.0',
'react-router-dom': '^5.0.0',
'@testing-library/react': '^11.0.0'
react: {
"16.x": {
"react-dom": "^16.0.0",
"react-router-dom": "^5.0.0",
"@testing-library/react": "^11.0.0",
},
'17.x': {
'react-dom': '^17.0.0',
'react-router-dom': '^5.0.0 || ^6.0.0',
'@testing-library/react': '^12.0.0'
"17.x": {
"react-dom": "^17.0.0",
"react-router-dom": "^5.0.0 || ^6.0.0",
"@testing-library/react": "^12.0.0",
},
'18.x': {
'react-dom': '^18.0.0',
'react-router-dom': '^6.0.0',
'@testing-library/react': '^13.0.0'
}
}
"18.x": {
"react-dom": "^18.0.0",
"react-router-dom": "^6.0.0",
"@testing-library/react": "^13.0.0",
},
},
};
function checkCompatibility(packages) {
@@ -95,6 +97,7 @@ function checkCompatibility(packages) {
## Staged Upgrade Strategy
### Phase 1: Planning
```bash
# 1. Identify current versions
npm list --depth=0
@@ -112,6 +115,7 @@ echo "Upgrade order:
```
### Phase 2: Incremental Updates
```bash
# Don't upgrade everything at once!
@@ -135,17 +139,18 @@ npm install react-router-dom@6
```
### Phase 3: Validation
```javascript
// tests/compatibility.test.js
describe('Dependency Compatibility', () => {
it('should have compatible React versions', () => {
const reactVersion = require('react/package.json').version;
const reactDomVersion = require('react-dom/package.json').version;
describe("Dependency Compatibility", () => {
it("should have compatible React versions", () => {
const reactVersion = require("react/package.json").version;
const reactDomVersion = require("react-dom/package.json").version;
expect(reactVersion).toBe(reactDomVersion);
});
it('should not have peer dependency warnings', () => {
it("should not have peer dependency warnings", () => {
// Run npm ls and check for warnings
});
});
@@ -154,6 +159,7 @@ describe('Dependency Compatibility', () => {
## Breaking Change Handling
### Identifying Breaking Changes
```bash
# Use changelog parsers
npx changelog-parser react 16.0.0 17.0.0
@@ -163,6 +169,7 @@ curl https://raw.githubusercontent.com/facebook/react/main/CHANGELOG.md
```
### Codemod for Automated Fixes
```bash
# React upgrade codemods
npx react-codeshift <transform> <path>
@@ -175,25 +182,26 @@ npx react-codeshift \
```
### Custom Migration Script
```javascript
// migration-script.js
const fs = require('fs');
const glob = require('glob');
const fs = require("fs");
const glob = require("glob");
glob('src/**/*.tsx', (err, files) => {
files.forEach(file => {
let content = fs.readFileSync(file, 'utf8');
glob("src/**/*.tsx", (err, files) => {
files.forEach((file) => {
let content = fs.readFileSync(file, "utf8");
// Replace old API with new API
content = content.replace(
/componentWillMount/g,
'UNSAFE_componentWillMount'
"UNSAFE_componentWillMount",
);
// Update imports
content = content.replace(
/import { Component } from 'react'/g,
"import React, { Component } from 'react'"
"import React, { Component } from 'react'",
);
fs.writeFileSync(file, content);
@@ -204,6 +212,7 @@ glob('src/**/*.tsx', (err, files) => {
## Testing Strategy
### Unit Tests
```javascript
// Ensure tests pass before and after upgrade
npm run test
@@ -213,26 +222,28 @@ npm install @testing-library/react@latest
```
### Integration Tests
```javascript
// tests/integration/app.test.js
describe('App Integration', () => {
it('should render without crashing', () => {
describe("App Integration", () => {
it("should render without crashing", () => {
render(<App />);
});
it('should handle navigation', () => {
it("should handle navigation", () => {
const { getByText } = render(<App />);
fireEvent.click(getByText('Navigate'));
expect(screen.getByText('New Page')).toBeInTheDocument();
fireEvent.click(getByText("Navigate"));
expect(screen.getByText("New Page")).toBeInTheDocument();
});
});
```
### Visual Regression Tests
```javascript
// visual-regression.test.js
describe('Visual Regression', () => {
it('should match snapshot', () => {
describe("Visual Regression", () => {
it("should match snapshot", () => {
const { container } = render(<App />);
expect(container.firstChild).toMatchSnapshot();
});
@@ -240,15 +251,16 @@ describe('Visual Regression', () => {
```
### E2E Tests
```javascript
// cypress/e2e/app.cy.js
describe('E2E Tests', () => {
it('should complete user flow', () => {
cy.visit('/');
describe("E2E Tests", () => {
it("should complete user flow", () => {
cy.visit("/");
cy.get('[data-testid="login"]').click();
cy.get('input[name="email"]').type('user@example.com');
cy.get('input[name="email"]').type("user@example.com");
cy.get('button[type="submit"]').click();
cy.url().should('include', '/dashboard');
cy.url().should("include", "/dashboard");
});
});
```
@@ -256,6 +268,7 @@ describe('E2E Tests', () => {
## Automated Dependency Updates
### Renovate Configuration
```json
// renovate.json
{
@@ -277,6 +290,7 @@ describe('E2E Tests', () => {
```
### Dependabot Configuration
```yaml
# .github/dependabot.yml
version: 2
@@ -322,6 +336,7 @@ fi
## Common Upgrade Patterns
### Lock File Management
```bash
# npm
npm install --package-lock-only # Update lock file only
@@ -333,6 +348,7 @@ yarn upgrade-interactive # Interactive upgrades
```
### Peer Dependency Resolution
```bash
# npm 7+: strict peer dependencies
npm install --legacy-peer-deps # Ignore peer deps
@@ -342,6 +358,7 @@ npm install --force
```
### Workspace Upgrades
```bash
# Update all workspace packages
npm install --workspaces
@@ -375,6 +392,7 @@ npm install package@latest --workspace=packages/app
```markdown
Pre-Upgrade:
- [ ] Review current dependency versions
- [ ] Read changelogs for breaking changes
- [ ] Create feature branch
@@ -382,6 +400,7 @@ Pre-Upgrade:
- [ ] Run full test suite (baseline)
During Upgrade:
- [ ] Upgrade one dependency at a time
- [ ] Update peer dependencies
- [ ] Fix TypeScript errors
@@ -390,6 +409,7 @@ During Upgrade:
- [ ] Check bundle size impact
Post-Upgrade:
- [ ] Full regression testing
- [ ] Performance testing
- [ ] Update documentation

View File

@@ -24,12 +24,14 @@ Master React version upgrades, class to hooks migration, concurrent features ado
**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)
@@ -39,6 +41,7 @@ Master React version upgrades, class to hooks migration, concurrent features ado
## Class to Hooks Migration
### State Management
```javascript
// Before: Class component
class Counter extends React.Component {
@@ -46,13 +49,13 @@ class Counter extends React.Component {
super(props);
this.state = {
count: 0,
name: ''
name: "",
};
}
increment = () => {
this.setState({ count: this.state.count + 1 });
}
};
render() {
return (
@@ -67,7 +70,7 @@ class Counter extends React.Component {
// After: Functional component with hooks
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
const [name, setName] = useState("");
const increment = () => {
setCount(count + 1);
@@ -83,6 +86,7 @@ function Counter() {
```
### Lifecycle Methods to Hooks
```javascript
// Before: Lifecycle methods
class DataFetcher extends React.Component {
@@ -155,6 +159,7 @@ function DataFetcher({ id }) {
```
### Context and HOCs to Hooks
```javascript
// Before: Context consumer and HOC
const ThemeContext = React.createContext();
@@ -175,11 +180,7 @@ class ThemedButton extends React.Component {
function ThemedButton({ children }) {
const { theme } = useContext(ThemeContext);
return (
<button style={{ background: theme }}>
{children}
</button>
);
return <button style={{ background: theme }}>{children}</button>;
}
// Before: HOC for data fetching
@@ -188,7 +189,7 @@ function withUser(Component) {
state = { user: null };
componentDidMount() {
fetchUser().then(user => this.setState({ user }));
fetchUser().then((user) => this.setState({ user }));
}
render() {
@@ -218,52 +219,55 @@ function UserProfile() {
## React 18 Concurrent Features
### New Root API
```javascript
// Before: React 17
import ReactDOM from 'react-dom';
import ReactDOM from "react-dom";
ReactDOM.render(<App />, document.getElementById('root'));
ReactDOM.render(<App />, document.getElementById("root"));
// After: React 18
import { createRoot } from 'react-dom/client';
import { createRoot } from "react-dom/client";
const root = createRoot(document.getElementById('root'));
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);
setCount((c) => c + 1);
setFlag((f) => !f);
// Only one re-render (batched)
}
// Even in async:
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
setCount((c) => c + 1);
setFlag((f) => !f);
// Still batched in React 18!
}, 1000);
// Opt out if needed
import { flushSync } from 'react-dom';
import { flushSync } from "react-dom";
flushSync(() => {
setCount(c => c + 1);
setCount((c) => c + 1);
});
// Re-render happens here
setFlag(f => !f);
setFlag((f) => !f);
// Another re-render
```
### Transitions
```javascript
import { useState, useTransition } from 'react';
import { useState, useTransition } from "react";
function SearchResults() {
const [query, setQuery] = useState('');
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
@@ -288,8 +292,9 @@ function SearchResults() {
```
### Suspense for Data Fetching
```javascript
import { Suspense } from 'react';
import { Suspense } from "react";
// Resource-based data fetching (with React 18)
const resource = fetchProfileData();
@@ -320,6 +325,7 @@ function ProfileTimeline() {
## Codemods for Automation
### Run React Codemods
```bash
# Install jscodeshift
npm install -g jscodeshift
@@ -342,22 +348,25 @@ npx codemod react/hooks/convert-class-to-function src/
```
### Custom Codemod Example
```javascript
// custom-codemod.js
module.exports = function(file, api) {
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
});
root
.find(j.CallExpression, {
callee: {
type: "MemberExpression",
property: { name: "setState" },
},
})
.forEach((path) => {
// Transform to useState
// ... transformation logic
});
return root.toSource();
};
@@ -368,38 +377,38 @@ module.exports = function(file, api) {
## Performance Optimization
### useMemo and useCallback
```javascript
function ExpensiveComponent({ items, filter }) {
// Memoize expensive calculation
const filteredItems = useMemo(() => {
return items.filter(item => item.category === filter);
return items.filter((item) => item.category === filter);
}, [items, filter]);
// Memoize callback to prevent child re-renders
const handleClick = useCallback((id) => {
console.log('Clicked:', id);
console.log("Clicked:", id);
}, []); // No dependencies, never changes
return (
<List items={filteredItems} onClick={handleClick} />
);
return <List items={filteredItems} onClick={handleClick} />;
}
// Child component with memo
const List = React.memo(({ items, onClick }) => {
return items.map(item => (
return items.map((item) => (
<Item key={item.id} item={item} onClick={onClick} />
));
});
```
### Code Splitting
```javascript
import { lazy, Suspense } from 'react';
import { lazy, Suspense } from "react";
// Lazy load components
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));
const Dashboard = lazy(() => import("./Dashboard"));
const Settings = lazy(() => import("./Settings"));
function App() {
return (
@@ -446,12 +455,14 @@ function List<T>({ items, renderItem }: ListProps<T>) {
```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
@@ -461,6 +472,7 @@ function List<T>({ items, renderItem }: ListProps<T>) {
- [ ] 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
@@ -470,6 +482,7 @@ function List<T>({ items, renderItem }: ListProps<T>) {
- [ ] Adopt Suspense/Transitions where beneficial
### Performance
- [ ] Identify performance bottlenecks
- [ ] Add React.memo where appropriate
- [ ] Use useMemo/useCallback for expensive operations
@@ -477,6 +490,7 @@ function List<T>({ items, renderItem }: ListProps<T>) {
- [ ] Optimize re-renders
### Testing
- [ ] Update test utilities (React Testing Library)
- [ ] Test with React 18 features
- [ ] Check for warnings in console