Skip to content

Component Transformation Guide

Master the art of transforming complex React components into clean, testable templates using TDI2 service injection.

🎯 Transformation Goals

  • Zero Data Props - Components receive only services
  • Pure Templates - Components focus solely on rendering
  • Automatic Reactivity - State updates trigger re-renders
  • Easy Testing - Simple service mocking

// ❌ Props hell + complex state management
function ProductList({
products,
loading,
category,
onCategoryChange,
onAddToCart,
cartItems,
user,
/* ...15+ more props */
}) {
const [filteredProducts, setFilteredProducts] = useState([]);
const [searchQuery, setSearchQuery] = useState('');
useEffect(() => {
// Complex filtering logic
const filtered = products.filter(p =>
p.category === category &&
p.name.includes(searchQuery)
);
setFilteredProducts(filtered);
}, [products, category, searchQuery]);
// 50+ lines of state management logic
return <div>{/* Complex JSX */}</div>;
}
// ✅ Clean service injection
function ProductList({
productService,
cartService
}: {
productService: Inject<ProductServiceInterface>;
cartService: Inject<CartServiceInterface>;
}) {
const { filteredProducts, searchQuery, loading } = productService.state;
return (
<div className="product-list">
<SearchInput
value={searchQuery}
onChange={(query) => productService.setSearchQuery(query)}
/>
{loading ? (
<ProductListSkeleton />
) : (
<div className="products-grid">
{filteredProducts.map(product => (
<ProductCard
key={product.id}
product={product}
onAddToCart={() => cartService.addProduct(product)}
/>
))}
</div>
)}
</div>
);
}

Move all state management and business logic into services:

// Extract this logic from components
interface ProductCatalogServiceInterface {
state: {
products: Product[];
filteredProducts: Product[];
searchQuery: string;
loading: boolean;
};
setSearchQuery(query: string): void;
setCategory(category: string): void;
addToCart(product: Product): void;
}
@Service()
export class ProductCatalogService implements ProductCatalogServiceInterface {
state = {
products: [] as Product[],
filteredProducts: [] as Product[],
searchQuery: '',
loading: false
};
constructor(@Inject() private cartService: CartService) {}
setSearchQuery(query: string): void {
this.state.searchQuery = query;
this.filterProducts();
}
private filterProducts(): void {
this.state.filteredProducts = this.state.products.filter(product =>
product.name.toLowerCase().includes(this.state.searchQuery.toLowerCase())
);
}
}

Replace props with service injection:

// Before: Multiple data props
interface ProductListProps {
products: Product[];
loading: boolean;
onAddToCart: (product: Product) => void;
// ... many more props
}
// After: Service injection only
interface ProductListProps {
productService: Inject<ProductCatalogServiceInterface>;
cartService: Inject<CartServiceInterface>;
}

Remove all useState, useEffect, and business logic:

function ProductList({ productService, cartService }: ProductListProps) {
// No useState or useEffect needed!
const { filteredProducts, searchQuery, loading } = productService.state;
return (
<div>
{/* Pure template - only rendering and event handlers */}
</div>
);
}

interface UserFormProps {
userFormService: Inject<UserFormServiceInterface>;
}
function UserForm({ userFormService }: UserFormProps) {
const { formData, errors, saving } = userFormService.state;
return (
<form onSubmit={userFormService.handleSubmit}>
<input
value={formData.name}
onChange={(e) => userFormService.updateField('name', e.target.value)}
error={errors.name}
/>
<button type="submit" disabled={saving}>
{saving ? 'Saving...' : 'Save'}
</button>
</form>
);
}

Key Features:

  • All form state in service
  • Validation handled by service
  • Component only renders and handles events
function UserTable({ userTableService }: {
userTableService: Inject<UserTableServiceInterface>;
}) {
const { displayedUsers, sortBy, sortOrder } = userTableService.state;
return (
<table>
<thead>
<tr>
<th onClick={() => userTableService.setSorting('name')}>
Name {sortBy === 'name' && (sortOrder === 'asc' ? '' : '')}
</th>
<th onClick={() => userTableService.setSorting('email')}>
Email {sortBy === 'email' && (sortOrder === 'asc' ? '' : '')}
</th>
</tr>
</thead>
<tbody>
{displayedUsers.map(user => (
<tr key={user.id}>
<td>{user.name}</td>
<td>{user.email}</td>
</tr>
))}
</tbody>
</table>
);
}

Key Features:

  • Sorting, filtering, pagination in service
  • Reactive table updates
  • Clean separation of concerns
function UserModal({ userModalService }: {
userModalService: Inject<UserModalServiceInterface>;
}) {
const { isOpen, currentUser, editing } = userModalService.state;
return (
<Modal
isOpen={isOpen}
onClose={() => userModalService.close()}
>
{editing ? (
<UserEditForm
user={currentUser}
onSave={(data) => userModalService.saveUser(data)}
onCancel={() => userModalService.cancelEditing()}
/>
) : (
<UserDisplay
user={currentUser}
onEdit={() => userModalService.startEditing()}
/>
)}
</Modal>
);
}

Key Features:

  • Modal state managed by service
  • Conditional rendering based on service state
  • Clean modal lifecycle management

describe('ProductCatalogService', () => {
it('should filter products by search query', () => {
const service = new ProductCatalogService();
service.state.products = [
{ id: '1', name: 'iPhone', category: 'phones' },
{ id: '2', name: 'MacBook', category: 'laptops' }
];
service.setSearchQuery('iPhone');
expect(service.state.filteredProducts).toHaveLength(1);
expect(service.state.filteredProducts[0].name).toBe('iPhone');
});
});
describe('ProductList', () => {
it('should render filtered products', () => {
const mockService = {
state: {
filteredProducts: [{ id: '1', name: 'iPhone' }],
loading: false
},
setSearchQuery: jest.fn()
};
render(<ProductList productService={mockService} />);
expect(screen.getByText('iPhone')).toBeInTheDocument();
});
});

  • Identify all useState calls
  • List all useEffect dependencies
  • Note business logic mixed in component
  • Count props being passed down
  • Create service interface
  • Move all state to service
  • Move business logic to service methods
  • Add service dependencies via @Inject()
  • Replace props with service injection
  • Remove all useState and useEffect
  • Update event handlers to call service methods
  • Verify component only contains JSX and handlers
  • Write service unit tests for business logic
  • Write component tests for rendering behavior
  • Mock services for component tests
  • Verify separation of concerns

Each component should have one clear rendering purpose.

Always inject interfaces, never concrete classes.

Use arrow functions for service method calls in event handlers.

Base conditions on service state, not props.

Don’t mix service state with local useState.


🎯 Key Takeaway

Transform components incrementally. Start with the most complex components first - the ones with the most props and state management. The benefits become immediately apparent.