UTA DevHub

Component Architecture

Fundamental patterns and organization strategies for React Native components

Component Architecture

Overview

This section covers the fundamental architectural patterns for building React Native components. Understanding these patterns helps create consistent, maintainable, and scalable component architectures.

Architectural Goals

Our component architecture aims to:

  • Separate concerns between logic and presentation
  • Maximize reusability across different contexts
  • Ensure testability in isolation
  • Optimize performance by default

Core Components/Architecture

Component Types and Patterns

1. Pure Functional Components

The default pattern for all components. Pure components are predictable and easy to test.

// ui/foundation/Button/Button.tsx
import React from 'react';
import { TouchableOpacity, Text, ActivityIndicator } from 'react-native';
import { styles } from './styles';
import type { ButtonProps } from './types';
 
export const Button: React.FC<ButtonProps> = ({
  title,
  onPress,
  variant = 'primary',
  size = 'medium',
  disabled = false,
  loading = false,
  testID,
  ...rest
}) => {
  const isDisabled = disabled || loading;
 
  return (
    <TouchableOpacity
      style={[
        styles.button,
        styles[variant],
        styles[size],
        isDisabled && styles.disabled,
      ]}
      onPress={onPress}
      disabled={isDisabled}
      testID={testID}
      {...rest}
    >
      {loading ? (
        <ActivityIndicator color={styles[variant].color} />
      ) : (
        <Text style={[styles.text, styles[`${variant}Text`]]}>{title}</Text>
      )}
    </TouchableOpacity>
  );
};

2. Container/Presentational Pattern

Container Component (Smart)

Handles data fetching, state management, and business logic:

// features/products/components/ProductList/ProductListContainer.tsx
import React from 'react';
import { useProducts } from '@/core/domains/products/hooks';
import { ProductList } from './ProductList';
import { LoadingView, ErrorView } from '@/ui';
 
export const ProductListContainer: React.FC = () => {
  const { data: products, isLoading, error, refetch } = useProducts();
 
  if (isLoading) return <LoadingView />;
  if (error) return <ErrorView error={error} onRetry={refetch} />;
 
  return (
    <ProductList 
      products={products} 
      onRefresh={refetch}
    />
  );
};

Presentational Component (Dumb)

Focuses purely on UI rendering:

// features/products/components/ProductList/ProductList.tsx
import React from 'react';
import { FlatList, RefreshControl } from 'react-native';
import { ProductCard } from '../ProductCard';
import { EmptyState } from '@/ui';
import type { Product } from '@/core/domains/products/types';
import { styles } from './styles';
 
interface ProductListProps {
  products: Product[];
  onRefresh?: () => void;
  refreshing?: boolean;
}
 
export const ProductList: React.FC<ProductListProps> = ({ 
  products, 
  onRefresh,
  refreshing = false 
}) => {
  return (
    <FlatList
      data={products}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => <ProductCard product={item} />}
      contentContainerStyle={styles.container}
      ItemSeparatorComponent={() => <View style={styles.separator} />}
      ListEmptyComponent={<EmptyState message="No products found" />}
      refreshControl={
        onRefresh ? (
          <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
        ) : undefined
      }
    />
  );
};

3. Compound Component Pattern

For complex components with multiple related parts:

// ui/Card/Card.tsx
import React, { createContext, useContext } from 'react';
import { View, Text } from 'react-native';
import { styles } from './styles';
import type { CardProps, CardContextValue } from './types';
 
// Context for sharing state between compound components
const CardContext = createContext<CardContextValue | undefined>(undefined);
 
// Main component with sub-components
export const Card: React.FC<CardProps> & {
  Header: typeof CardHeader;
  Body: typeof CardBody;
  Footer: typeof CardFooter;
} = ({ children, variant = 'default', elevated = false }) => {
  const contextValue: CardContextValue = { variant, elevated };
 
  return (
    <CardContext.Provider value={contextValue}>
      <View style={[
        styles.container, 
        styles[variant],
        elevated && styles.elevated
      ]}>
        {children}
      </View>
    </CardContext.Provider>
  );
};
 
// Sub-components
const CardHeader: React.FC<CardHeaderProps> = ({ title, subtitle, action }) => {
  const context = useContext(CardContext);
  
  return (
    <View style={styles.header}>
      <View style={styles.headerText}>
        <Text style={[styles.title, styles[`${context?.variant}Title`]]}>
          {title}
        </Text>
        {subtitle && (
          <Text style={styles.subtitle}>{subtitle}</Text>
        )}
      </View>
      {action && <View style={styles.headerAction}>{action}</View>}
    </View>
  );
};
 
const CardBody: React.FC<CardBodyProps> = ({ children, padded = true }) => (
  <View style={[styles.body, padded && styles.bodyPadded]}>
    {children}
  </View>
);
 
const CardFooter: React.FC<CardFooterProps> = ({ children, divider = true }) => (
  <View style={[styles.footer, divider && styles.footerDivider]}>
    {children}
  </View>
);
 
// Attach sub-components
Card.Header = CardHeader;
Card.Body = CardBody;
Card.Footer = CardFooter;
 
// Usage example:
<Card variant="elevated" elevated>
  <Card.Header 
    title="Product Details" 
    subtitle="Premium Collection"
    action={<IconButton icon="more-vert" onPress={handleMore} />}
  />
  <Card.Body>
    <ProductDetails product={product} />
  </Card.Body>
  <Card.Footer>
    <Button title="Add to Cart" onPress={handleAddToCart} />
  </Card.Footer>
</Card>

4. Higher-Order Component Pattern

For adding cross-cutting functionality:

// ui/hoc/withLoading.tsx
import React from 'react';
import { View, ActivityIndicator } from 'react-native';
import { styles } from './styles';
 
interface WithLoadingProps {
  isLoading: boolean;
  loadingComponent?: React.ReactElement;
}
 
export function withLoading<P extends object>(
  Component: React.ComponentType<P>
): React.FC<P & WithLoadingProps> {
  return ({ isLoading, loadingComponent, ...props }) => {
    if (isLoading) {
      return loadingComponent || (
        <View style={styles.loadingContainer}>
          <ActivityIndicator size="large" />
        </View>
      );
    }
    
    return <Component {...(props as P)} />;
  };
}
 
// Usage
const ProductListWithLoading = withLoading(ProductList);
 
// In component
<ProductListWithLoading 
  isLoading={loading} 
  products={products} 
/>

5. Render Props Pattern

For maximum flexibility in rendering:

// ui/DataProvider/DataProvider.tsx
interface DataProviderProps<T> {
  url: string;
  params?: Record<string, any>;
  children: (props: {
    data: T | null;
    loading: boolean;
    error: Error | null;
    refetch: () => void;
  }) => React.ReactElement;
}
 
export function DataProvider<T>({ url, params, children }: DataProviderProps<T>) {
  const [state, setState] = useState<{
    data: T | null;
    loading: boolean;
    error: Error | null;
  }>({
    data: null,
    loading: true,
    error: null,
  });
 
  const fetchData = useCallback(async () => {
    setState(prev => ({ ...prev, loading: true, error: null }));
    
    try {
      const response = await api.get<T>(url, { params });
      setState({ data: response.data, loading: false, error: null });
    } catch (error) {
      setState({ data: null, loading: false, error: error as Error });
    }
  }, [url, params]);
 
  useEffect(() => {
    fetchData();
  }, [fetchData]);
 
  return children({
    ...state,
    refetch: fetchData,
  });
}
 
// Usage
<DataProvider<Product[]> url="/api/products" params={{ category: 'electronics' }}>
  {({ data, loading, error, refetch }) => {
    if (loading) return <LoadingView />;
    if (error) return <ErrorView error={error} onRetry={refetch} />;
    return <ProductList products={data!} onRefresh={refetch} />;
  }}
</DataProvider>

Component Data Flow

Data Flow Best Practices

  1. Unidirectional Data Flow: Data flows down through props, events flow up through callbacks
  2. Single Source of Truth: Each piece of state should have one authoritative source
  3. Immutable Updates: Never mutate state directly
  4. Minimal State: Only store what you can't derive
// Example of proper data flow
const ParentComponent: React.FC = () => {
  const [selectedId, setSelectedId] = useState<string | null>(null);
  
  const handleSelect = (id: string) => {
    setSelectedId(id);
  };
  
  return (
    <View>
      <ChildComponent 
        selectedId={selectedId}
        onSelect={handleSelect}
      />
      <DetailView itemId={selectedId} />
    </View>
  );
};

Design Decisions and Trade-offs

Pattern Selection Guide

PatternUse WhenAvoid When
Pure Components- Simple UI with no side effects
- Props determine output
- Need high reusability
- Complex state management
- Side effects needed
Container/Presenter- Clear separation needed
- Complex business logic
- Different data sources
- Simple components
- Overhead not justified
Compound Components- Related UI elements
- Flexible composition
- Shared state needed
- Simple, standalone components
- No relationship between parts
HOCs- Cross-cutting concerns
- Reusable enhancements
- Legacy codebases
- Hooks can solve it
- Deep composition needed
Render Props- Maximum flexibility
- Dynamic rendering
- Inversion of control
- Simple cases
- Performance critical

Architecture Principles

  1. Composition Over Configuration

    // ✅ Good - Composable
    <Card>
      <Card.Header title="Title" />
      <Card.Body>{content}</Card.Body>
    </Card>
     
    // ❌ Avoid - Configuration heavy
    <Card 
      headerTitle="Title"
      bodyContent={content}
      showHeader={true}
      showBody={true}
    />
  2. Explicit Over Implicit

    // ✅ Good - Explicit props
    <Button 
      variant="primary"
      size="large"
      disabled={!isValid}
    />
     
    // ❌ Avoid - Implicit behavior
    <Button primary large />
  3. Isolated Over Coupled

    // ✅ Good - Isolated component
    <ProductCard 
      product={product}
      onPress={() => navigate('ProductDetail', { id: product.id })}
    />
     
    // ❌ Avoid - Coupled to navigation
    <ProductCard 
      product={product}
      navigation={navigation}
    />

Summary

Component architecture patterns provide:

  • Structure for organizing component code
  • Separation of concerns between logic and UI
  • Flexibility in composition and reuse
  • Consistency across the codebase
  • Testability through isolation

Choose patterns based on your specific needs, keeping simplicity and maintainability as primary goals.

Next Steps