UTA DevHub

Feature Integration

Composing UI components within feature modules

Feature Integration

Overview

Feature integration focuses on composing UI components within feature modules, ensuring proper organization and maintainability. This guide covers patterns for structuring feature-specific UI components and their integration with the broader application architecture.

Design Principles

Feature Encapsulation

  • Feature-specific UI components stay within feature boundaries
  • Clear separation between feature and shared components
  • Feature-specific state management

Why it matters: Encapsulation ensures features can be developed, tested, and maintained independently. It prevents tight coupling between features, allows for parallel development, and makes features more portable. Well-encapsulated features can often be toggled on/off without affecting the rest of the application.

Feature Structure

Directory Organization

features/
└── product-catalog/
    ├── screens/
    │   ├── ProductListScreen.tsx
    │   ├── ProductDetailScreen.tsx
    │   └── index.ts
    ├── components/
    │   ├── ProductFilter.tsx
    │   ├── ProductSort.tsx
    │   └── index.ts
    ├── hooks/
    │   ├── useProductList.ts
    │   └── useProductFilter.ts
    ├── navigation/
    │   └── ProductCatalogNavigator.tsx
    └── index.ts

Component Integration

// features/product-catalog/screens/ProductListScreen.tsx
import { ProductCard } from '@/ui/business/ProductCard';
import { SearchBox } from '@/ui/patterns/SearchBox';
import { useProductList } from '../hooks/useProductList';
 
export function ProductListScreen() {
  const { products, loading, error } = useProductList();
  
  return (
    <Screen>
      <SearchBox />
      <ProductList>
        {products.map(product => (
          <ProductCard
            key={product.id}
            productId={product.id}
          />
        ))}
      </ProductList>
    </Screen>
  );
}

Integration Patterns

1. Feature-Specific Components

// features/product-catalog/components/ProductFilter.tsx
import { FilterButton } from '@/ui/patterns/FilterButton';
import { useProductFilter } from '../hooks/useProductFilter';
 
export function ProductFilter() {
  const { filters, setFilter } = useProductFilter();
  
  return (
    <View>
      {filters.map(filter => (
        <FilterButton
          key={filter.id}
          active={filter.active}
          onPress={() => setFilter(filter.id)}
        >
          {filter.label}
        </FilterButton>
      ))}
    </View>
  );
}

2. Navigation Integration

// features/product-catalog/navigation/ProductCatalogNavigator.tsx
import { createStackNavigator } from '@react-navigation/stack';
import { ProductListScreen } from '../screens/ProductListScreen';
import { ProductDetailScreen } from '../screens/ProductDetailScreen';
 
const Stack = createStackNavigator();
 
export function ProductCatalogNavigator() {
  return (
    <Stack.Navigator>
      <Stack.Screen
        name="ProductList"
        component={ProductListScreen}
        options={{
          title: 'Products'
        }}
      />
      <Stack.Screen
        name="ProductDetail"
        component={ProductDetailScreen}
        options={{
          title: 'Product Details'
        }}
      />
    </Stack.Navigator>
  );
}

3. State Management

// features/product-catalog/hooks/useProductList.ts
import { useState, useEffect } from 'react';
import { useProducts } from '@/core/domains/products/hooks';
 
export function useProductList() {
  const [filters, setFilters] = useState<ProductFilter[]>([]);
  const { products, loading, error } = useProducts();
  
  const filteredProducts = useMemo(() => {
    return products.filter(product => 
      filters.every(filter => filter.predicate(product))
    );
  }, [products, filters]);
  
  return {
    products: filteredProducts,
    loading,
    error,
    filters,
    setFilters
  };
}

Best Practices

Do's ✅

  • (Do ✅) Keep feature-specific components within feature boundaries
  • (Do ✅) Use business components for domain integration
  • (Do ✅) Implement proper loading and error states
  • (Do ✅) Follow consistent navigation patterns
  • (Do ✅) Maintain clear component hierarchy

Don'ts ❌

  • (Don't ❌) Create feature-specific foundation components
  • (Don't ❌) Duplicate business logic in feature components
  • (Don't ❌) Mix feature-specific and shared state
  • (Don't ❌) Skip proper error handling
  • (Don't ❌) Create tight coupling between features

Testing Strategies

// features/product-catalog/components/__tests__/ProductFilter.test.tsx
describe('ProductFilter', () => {
  it('applies filter when button is pressed', () => {
    const { getByText } = render(<ProductFilter />);
    
    fireEvent.press(getByText('Price: Low to High'));
    
    expect(getByText('Price: Low to High')).toHaveStyle({
      backgroundColor: expect.any(String)
    });
  });
  
  it('tracks filter usage with analytics', () => {
    const mockAnalytics = jest.spyOn(analyticsService, 'trackEvent');
    const { getByText } = render(<ProductFilter />);
    
    fireEvent.press(getByText('Price: Low to High'));
    
    expect(mockAnalytics).toHaveBeenCalledWith({
      type: 'filter_apply',
      filterId: 'price_low_to_high'
    });
  });
});
// features/product-catalog/screens/__tests__/ProductListScreen.test.tsx
describe('ProductListScreen', () => {
  it('navigates to product detail on press', () => {
    const { getByTestId } = render(<ProductListScreen />);
    
    fireEvent.press(getByTestId('product-card-123'));
    
    expect(mockNavigation.navigate).toHaveBeenCalledWith(
      'ProductDetail',
      { productId: '123' }
    );
  });
  
  it('displays products from domain hook', async () => {
    // Mock domain hook response
    useProducts.mockReturnValue({
      products: mockProducts,
      loading: false
    });
    
    const { getAllByTestId } = render(<ProductListScreen />);
    
    await waitFor(() => {
      expect(getAllByTestId('product-card')).toHaveLength(mockProducts.length);
    });
  });
});
// features/product-catalog/navigation/__tests__/ProductCatalogNavigator.test.tsx
describe('ProductCatalogNavigator', () => {
  it('renders the correct initial screen', () => {
    const { getByTestId } = render(<ProductCatalogNavigator />);
    
    expect(getByTestId('product-list-screen')).toBeTruthy();
  });
  
  it('handles deep linking correctly', () => {
    const { getByText } = render(
      <NavigationContainer linking={linkingConfig}>
        <ProductCatalogNavigator />
      </NavigationContainer>
    );
    
    // Simulate deep link
    const { getState } = navigationRef.current;
    const state = getState();
    
    expect(state.routes[0].name).toBe('ProductDetail');
    expect(state.routes[0].params).toEqual({ productId: '123' });
  });
});

On this page