Skip to content

Migration Strategy

From Props Hell to Service-Oriented Architecture

Section titled “From Props Hell to Service-Oriented Architecture”

Transform existing React applications to TDI2 with a proven, risk-minimized approach that maintains development velocity throughout the transition.

🎯 Migration Benefits

  • 90% Reduction - Components with complex prop chains
  • 50% Faster - Test setup and maintenance
  • 25% Improvement - Development velocity after transition
  • Zero Downtime - Incremental migration approach

  • Components with 10+ props
  • Props passed through 3+ component levels
  • Frequent prop threading changes during development
  • Complex prop validation and default value management
  • Multiple state management solutions (Redux + Context + useState)
  • Manual state synchronization between components
  • Complex useEffect dependency arrays
  • Difficult state debugging needs
  • Component tests requiring complex mock setups
  • Difficulty isolating business logic for testing
  • High test maintenance overhead when props change
  • Inconsistent testing patterns across teams
CriteriaScore (1-5)Weight
Props complexity___25%
State management pain___25%
Testing difficulty___20%
Team scalability issues___20%
Technical debt level___10%

Migration Recommendation:

  • 4.0-5.0: High priority - Immediate TDI2 adoption recommended
  • 3.0-3.9: Medium priority - Plan migration within 6 months
  • 2.0-2.9: Low priority - Monitor and reassess in 1 year
  • < 2.0: Focus on other architectural improvements first

Goal: Establish TDI2 infrastructure without disrupting development

Terminal window
# Install TDI2 dependencies
npm install @tdi2/di-core @tdi2/vite-plugin-di valtio
vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { diEnhancedPlugin } from '@tdi2/vite-plugin-di';
export default defineConfig({
plugins: [
diEnhancedPlugin({
enableInterfaceResolution: true,
enableFunctionalDI: true,
generateDebugFiles: process.env.NODE_ENV === 'development'
}),
react(),
],
});
tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": false,
"target": "ES2020"
}
}
src/
├── services/ # NEW: Service layer
│ ├── interfaces/ # Service contracts
│ └── implementations/ # Service implementations
├── repositories/ # NEW: Data access layer
│ ├── interfaces/ # Repository contracts
│ └── implementations/ # API/Mock implementations
├── components/ # EXISTING: Gradually migrate
├── hooks/ # EXISTING: Gradually deprecate
├── store/ # EXISTING: Gradually replace
└── types/ # EXISTING: Expand with service types
  • Architecture workshop (4 hours) - Service-oriented principles
  • Hands-on coding (4 hours) - Create first e-commerce service
  • Best practices - Coding standards and testing requirements

Goal: Validate TDI2 with one complete high-value feature

Sprint 1 (Week 4): Service Layer Foundation

  • Day 1-2: Create service interfaces for pilot feature
  • Day 3-5: Implement core business logic services
  • Day 6-7: Add repository abstractions and mocks
  • Day 8-10: Unit test service layer in isolation

Sprint 2 (Week 5-6): Component Transformation

  • Day 1-3: Transform pilot components to use service injection
  • Day 4-6: Remove props and hook dependencies
  • Day 7-8: Update component tests to use service mocks
  • Day 9-10: Integration testing and performance validation

Sprint 3 (Week 7): Integration & Polish

  • Day 1-3: Connect to real data sources through repositories
  • Day 4-5: Performance optimization and bundle analysis
  • Day 6-7: Error handling and edge cases
  • Day 8-10: Feature flag rollout and user testing

Sprint 4 (Week 8): Validation & Documentation

  • Day 1-2: Metrics collection and performance comparison
  • Day 3-4: Team retrospective and lessons learned
  • Day 5-7: Documentation and architectural decision records
  • Day 8-10: Plan expansion to next features

Selection Criteria:

  • ✅ Self-contained with clear boundaries
  • ✅ Currently experiencing props hell (8+ props)
  • ✅ High business value for stakeholders
  • ✅ Good existing test coverage

Before: Traditional React

function ProductCatalog({
products, categories, loading, error, user, cart,
searchQuery, filters, sortBy, pagination, theme,
onSearch, onFilter, onSort, onAddToCart,
// ...12 more props
}: ProductCatalogProps) {
// 150+ lines of state coordination
// Complex useEffect chains
// Manual prop drilling
}

After: TDI2 Service-Oriented

function ProductCatalog({
productService,
cartService,
userService
}: {
productService: Inject<ProductServiceInterface>;
cartService: Inject<CartServiceInterface>;
userService: Inject<UserServiceInterface>;
}) {
const { products, loading, searchQuery } = productService.state;
const { user } = userService.state;
return (
<div className="product-catalog">
<SearchBar
query={searchQuery}
onSearch={(query) => productService.search(query)}
/>
<ProductGrid
products={products}
loading={loading}
onAddToCart={(product) => cartService.addItem(product)}
/>
</div>
);
}
// Service interface
interface ProductServiceInterface {
state: {
products: Product[];
categories: Category[];
loading: boolean;
searchQuery: string;
selectedCategory: string | null;
};
loadProducts(): Promise<void>;
search(query: string): void;
filterByCategory(category: string): void;
}
// Service implementation
@Service()
export class ProductService implements ProductServiceInterface {
state = {
products: [] as Product[],
categories: [] as Category[],
loading: false,
searchQuery: '',
selectedCategory: null as string | null
};
constructor(
@Inject() private productRepository: ProductRepository,
@Inject() private notificationService: NotificationService
) {}
async loadProducts(): Promise<void> {
this.state.loading = true;
try {
this.state.products = await this.productRepository.getProducts();
this.state.categories = await this.productRepository.getCategories();
} catch (error) {
this.notificationService.showError('Failed to load products');
} finally {
this.state.loading = false;
}
}
search(query: string): void {
this.state.searchQuery = query;
// Reactive filtering happens automatically
}
}

Phase 3: Incremental Expansion (Weeks 9-16)

Section titled “Phase 3: Incremental Expansion (Weeks 9-16)”

Goal: Systematically migrate remaining application

FeatureProps ComplexityBusiness ValueMigration EffortPriority
Shopping CartHigh (10+ props)HighMedium🔥🔥🔥🔥🔥
User DashboardHigh (8+ props)HighMedium🔥🔥🔥🔥
Product SearchMedium (6 props)MediumLow🔥🔥🔥
User SettingsMedium (5 props)MediumLow🔥🔥
Admin PanelHigh (12+ props)LowHigh🔥

Wave 1 (Weeks 9-12): High-Impact Features

  • Shopping cart and checkout flow
  • User dashboard and profile
  • Product search and filtering

Wave 2 (Weeks 13-16): Remaining Features

  • User settings and preferences
  • Admin features
  • Secondary workflows

Before: Redux Store

// Redux slice
const userSlice = createSlice({
name: 'user',
initialState: { currentUser: null, loading: false },
reducers: {
loadUserStart: (state) => { state.loading = true; },
loadUserSuccess: (state, action) => {
state.currentUser = action.payload;
state.loading = false;
},
loadUserError: (state) => { state.loading = false; }
}
});
// Component with useSelector
function UserProfile() {
const user = useSelector(state => state.user.currentUser);
const loading = useSelector(state => state.user.loading);
const dispatch = useDispatch();
useEffect(() => {
dispatch(loadUser());
}, []);
}

After: TDI2 Service

// Service with reactive state
@Service()
export class UserService implements UserServiceInterface {
state = {
currentUser: null as User | null,
loading: false
};
async loadUser(): Promise<void> {
this.state.loading = true;
try {
this.state.currentUser = await this.userRepository.getCurrentUser();
} finally {
this.state.loading = false;
}
}
}
// Component with service injection
function UserProfile({ userService }: {
userService: Inject<UserServiceInterface>;
}) {
const { currentUser, loading } = userService.state;
useEffect(() => {
userService.loadUser();
}, []);
}

Before: Context Provider

const ThemeContext = createContext();
const CartContext = createContext();
const UserContext = createContext();
function App() {
return (
<ThemeProvider>
<CartProvider>
<UserProvider>
<ProductCatalog />
</UserProvider>
</CartProvider>
</ThemeProvider>
);
}

After: DI Provider

function App() {
return (
<DIProvider container={container}>
<ProductCatalog /> {/* Services auto-injected */}
</DIProvider>
);
}

Before: Custom Hook

function useProducts() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(false);
const loadProducts = useCallback(async () => {
setLoading(true);
try {
const data = await api.getProducts();
setProducts(data);
} finally {
setLoading(false);
}
}, []);
return { products, loading, loadProducts };
}

After: Service

@Service()
export class ProductService {
state = {
products: [] as Product[],
loading: false
};
async loadProducts(): Promise<void> {
this.state.loading = true;
try {
this.state.products = await this.productRepository.getProducts();
} finally {
this.state.loading = false;
}
}
}

describe('ProductService', () => {
let productService: ProductService;
let mockRepository: jest.Mocked<ProductRepository>;
beforeEach(() => {
mockRepository = {
getProducts: jest.fn(),
getCategories: jest.fn()
};
productService = new ProductService(mockRepository, new MockNotificationService());
});
it('should load products successfully', async () => {
const mockProducts = [
{ id: '1', name: 'iPhone', price: 999 },
{ id: '2', name: 'MacBook', price: 1999 }
];
mockRepository.getProducts.mockResolvedValue(mockProducts);
await productService.loadProducts();
expect(productService.state.products).toEqual(mockProducts);
expect(productService.state.loading).toBe(false);
});
});
describe('ProductCatalog', () => {
it('should render products from service', () => {
const mockProductService = {
state: {
products: [{ id: '1', name: 'iPhone', price: 999 }],
loading: false
},
search: jest.fn()
};
render(<ProductCatalog productService={mockProductService} />);
expect(screen.getByText('iPhone')).toBeInTheDocument();
});
});

  • Learning Curve → Provide comprehensive training and pair programming
  • Performance Issues → Monitor metrics and maintain rollback plan
  • Integration Problems → Use adapter pattern for gradual transition
  • Development Velocity → Migrate high-value features first to show immediate benefit
  • Team Resistance → Start with teams experiencing most pain from current approach
  • Coordination Overhead → Clear migration timeline and regular checkpoints

  • 90% reduction in components with 5+ props
  • 50% reduction in test setup complexity
  • 30% improvement in bundle size
  • Zero performance regressions
  • 25% faster feature development velocity
  • 60% reduction in merge conflicts
  • 40% faster new developer onboarding
  • 95% developer satisfaction with new architecture

🎯 Key Takeaway

Start with your most painful feature, prove value quickly, then systematically expand. The incremental approach minimizes risk while maximizing team buy-in.