UTA DevHub

UI Integration Patterns

Best practices for integrating UI components with business logic and features

UI Integration Patterns

Overview

This section covers patterns and best practices for integrating UI components with our domain-driven architecture. Learn how to properly connect UI layers with business logic while maintaining clean separation of concerns.

Core Principle

UI components should remain focused on presentation while domains handle business logic. This separation enables:

  • Independent testing and development
  • Reusable components across features
  • Clear responsibilities and maintainability

Integration Topics

Architecture Overview

Our UI integration follows clear patterns based on component type:

Integration Rules

Foundation Components

Foundation components have the strictest integration rules:

// ui/foundation/Button/Button.tsx
import { theme } from '@/core/shared/styles/theme'; // ✅ Shared utilities
import { TouchableOpacity } from 'react-native';    // ✅ React Native
 
// ❌ NEVER import from:
// - core/domains/*
// - features/*
// - ui/business/*

Pattern Components

Pattern components can compose foundation components:

// ui/patterns/SearchBox/SearchBox.tsx
import { Input } from '@/ui/foundation/Input';      // ✅ Foundation components
import { IconButton } from '@/ui/foundation/IconButton'; // ✅ Foundation components
import { useDebounce } from '@/core/shared/hooks'; // ✅ Shared utilities
 
// ❌ NEVER import from:
// - core/domains/*
// - features/*
// - ui/business/*

Business Components

Business components integrate with domains:

// ui/business/ProductCard/ProductCard.tsx
import { Card } from '@/ui/foundation/Card';        // ✅ Foundation components
import { PriceTag } from '@/ui/patterns/PriceTag';  // ✅ Pattern components
import { useProduct } from '@/core/domains/products/hooks'; // ✅ Domain hooks
import { formatPrice } from '@/core/domains/products/utils'; // ✅ Domain utilities
 
// ❌ NEVER import from:
// - features/*

Common Integration Patterns

Pattern 1: Data Fetching in Business Components

// ui/business/UserAvatar/UserAvatar.tsx
import React from 'react';
import { Avatar } from '@/ui/foundation/Avatar';
import { useUser } from '@/core/domains/users/hooks';
import { Skeleton } from '@/ui/patterns/Skeleton';
 
interface UserAvatarProps {
  userId: string;
  size?: 'small' | 'medium' | 'large';
}
 
export function UserAvatar({ userId, size = 'medium' }: UserAvatarProps) {
  const { data: user, isLoading } = useUser(userId);
  
  if (isLoading) {
    return <Skeleton variant="circle" size={size} />;
  }
  
  return (
    <Avatar
      source={{ uri: user?.avatar }}
      fallback={user?.initials}
      size={size}
    />
  );
}

Pattern 2: Action Handling with Domain Services

// ui/business/AddToCartButton/AddToCartButton.tsx
import React from 'react';
import { Button } from '@/ui/foundation/Button';
import { useAddToCart } from '@/core/domains/cart/hooks';
import { HapticFeedback } from '@/core/shared/utils/haptic';
 
interface AddToCartButtonProps {
  productId: string;
  quantity?: number;
  variant?: 'primary' | 'secondary';
  onSuccess?: () => void;
}
 
export function AddToCartButton({ 
  productId, 
  quantity = 1,
  variant = 'primary',
  onSuccess 
}: AddToCartButtonProps) {
  const { mutate: addToCart, isLoading } = useAddToCart();
  
  const handlePress = () => {
    HapticFeedback.impact();
    addToCart(
      { productId, quantity },
      {
        onSuccess: () => {
          HapticFeedback.success();
          onSuccess?.();
        }
      }
    );
  };
  
  return (
    <Button
      variant={variant}
      loading={isLoading}
      onPress={handlePress}
    >
      Add to Cart
    </Button>
  );
}

Pattern 3: Complex State Management

For complex UI state that spans multiple components:

// features/product-catalog/state/filterStore.ts
import { create } from 'zustand';
 
interface FilterState {
  category: string | null;
  priceRange: [number, number];
  sortBy: 'price' | 'name' | 'rating';
  setCategory: (category: string | null) => void;
  setPriceRange: (range: [number, number]) => void;
  setSortBy: (sort: FilterState['sortBy']) => void;
  reset: () => void;
}
 
export const useFilterStore = create<FilterState>((set) => ({
  category: null,
  priceRange: [0, 1000],
  sortBy: 'name',
  setCategory: (category) => set({ category }),
  setPriceRange: (priceRange) => set({ priceRange }),
  setSortBy: (sortBy) => set({ sortBy }),
  reset: () => set({
    category: null,
    priceRange: [0, 1000],
    sortBy: 'name',
  }),
}));

Integration Guidelines

Do's ✅

  • (Do ✅) Keep foundation components pure - No business logic or domain imports
  • (Do ✅) Use business components for domain-aware UI elements
  • (Do ✅) Compose from smaller components - Build complex UI from simple pieces
  • (Do ✅) Handle loading states in business components that fetch data
  • (Do ✅) Use domain hooks for all business logic and API calls

Don'ts ❌

  • (Don't ❌) Import across features - Features should be independent
  • (Don't ❌) Put business logic in foundation/patterns - Keep them generic
  • (Don't ❌) Create circular dependencies - Follow the dependency flow
  • (Don't ❌) Bypass the architecture - Respect layer boundaries
  • (Don't ❌) Mix concerns - Each layer has its responsibility

Testing Integration

Test components at appropriate levels:

// Foundation component test - Pure UI
describe('Button', () => {
  it('renders with correct variant styles', () => {
    const { getByRole } = render(
      <Button variant="primary">Click me</Button>
    );
    // Test visual properties
  });
});
 
// Business component test - With mocked domain
jest.mock('@/core/domains/products/hooks');
 
describe('ProductCard', () => {
  it('displays product information', () => {
    useProduct.mockReturnValue({
      data: mockProduct,
      isLoading: false,
    });
    
    const { getByText } = render(
      <ProductCard productId="123" />
    );
    
    expect(getByText(mockProduct.name)).toBeTruthy();
  });
});

Performance Considerations

Optimize Re-renders

Use proper memoization for business components:

// ui/business/ProductList/ProductList.tsx
import { memo } from 'react';
import { FlatList } from 'react-native';
import { ProductCard } from '../ProductCard';
 
export const ProductList = memo(({ categoryId }: Props) => {
  const { data: products } = useProducts({ categoryId });
  
  const renderItem = useCallback(({ item }) => (
    <ProductCard key={item.id} productId={item.id} />
  ), []);
  
  return (
    <FlatList
      data={products}
      renderItem={renderItem}
      keyExtractor={item => item.id}
    />
  );
});

Lazy Load Heavy Components

Split bundles for better performance:

// Lazy load heavy business components
const ChartComponent = lazy(() => 
  import('@/ui/business/ChartComponent')
);
 
// Use with Suspense
<Suspense fallback={<Skeleton />}>
  <ChartComponent data={data} />
</Suspense>

Next Steps

Ready to implement proper UI integration?

  1. Learn Domain Integration patterns
  2. Master Feature Integration techniques
  3. Review State Management for complex state scenarios
  4. Study Communication Patterns for cross-feature needs

Remember: Clean integration patterns lead to maintainable, testable, and scalable applications. Respect the architecture boundaries and let each layer do what it does best.