Custom Components Guide

Learn how to create reusable, well-structured components following best practices.

Component Structure

Organize your components in the presentation layer:

typescript
src/presentation/
├── components/
│   ├── ui/              # Generic UI components
│   │   ├── Button.tsx
│   │   ├── Card.tsx
│   │   ├── Input.tsx
│   │   └── Modal.tsx
│   ├── domain/          # Domain-specific components
│   │   ├── TaskCard.tsx
│   │   ├── UserAvatar.tsx
│   │   └── PostList.tsx
│   └── layout/          # Layout components
│       ├── Header.tsx
│       ├── Footer.tsx
│       └── Container.tsx

Basic Component Template

src/presentation/components/ui/Button.tsx
import { Pressable, Text, ActivityIndicator, type PressableProps } from 'react-native';
import { cn } from '@/utils/cn';

interface ButtonProps extends Omit<PressableProps, 'children'> {
  children: string;
  variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  isLoading?: boolean;
  className?: string;
}

export function Button({
  children,
  variant = 'primary',
  size = 'md',
  isLoading,
  disabled,
  className,
  ...props
}: ButtonProps) {
  const baseStyles = 'rounded-lg active:opacity-80 transition-opacity';
  
  const variantStyles = {
    primary: 'bg-blue-500',
    secondary: 'bg-slate-200 dark:bg-slate-700',
    outline: 'border-2 border-blue-500 bg-transparent',
    ghost: 'bg-transparent',
  };

  const sizeStyles = {
    sm: 'px-4 py-2',
    md: 'px-6 py-3',
    lg: 'px-8 py-4',
  };

  const textStyles = {
    primary: 'text-white',
    secondary: 'text-slate-900 dark:text-white',
    outline: 'text-blue-500',
    ghost: 'text-blue-500',
  };

  const textSizeStyles = {
    sm: 'text-sm',
    md: 'text-base',
    lg: 'text-lg',
  };

  return (
    <Pressable
      disabled={disabled || isLoading}
      className={cn(
        baseStyles,
        variantStyles[variant],
        sizeStyles[size],
        (disabled || isLoading) && 'opacity-50',
        className
      )}
      {...props}
    >
      {isLoading ? (
        <ActivityIndicator
          size="small"
          color={variant === 'primary' ? 'white' : '#3b82f6'}
        />
      ) : (
        <Text
          className={cn(
            'font-semibold text-center',
            textStyles[variant],
            textSizeStyles[size]
          )}
        >
          {children}
        </Text>
      )}
    </Pressable>
  );
}

// Usage
<Button variant="primary" onPress={handleSubmit}>
  Submit
</Button>
<Button variant="outline" size="sm" onPress={handleCancel}>
  Cancel
</Button>
<Button variant="primary" isLoading>
  Loading...
</Button>

Component with Variants (CVA)

Use class-variance-authority for complex variant logic:

bash
npm install class-variance-authority
typescript
import { cva, type VariantProps } from 'class-variance-authority';

const cardVariants = cva(
  'rounded-xl p-6', // base styles
  {
    variants: {
      variant: {
        default: 'bg-white dark:bg-slate-800',
        elevated: 'bg-white dark:bg-slate-800 shadow-lg',
        outlined: 'border-2 border-slate-200 dark:border-slate-700',
        glass: 'bg-white/10 backdrop-blur-lg border border-white/20',
      },
      padding: {
        sm: 'p-3',
        md: 'p-6',
        lg: 'p-8',
      },
    },
    defaultVariants: {
      variant: 'default',
      padding: 'md',
    },
  }
);

interface CardProps extends VariantProps<typeof cardVariants> {
  children: React.ReactNode;
  className?: string;
}

export function Card({ children, variant, padding, className }: CardProps) {
  return (
    <View className={cn(cardVariants({ variant, padding }), className)}>
      {children}
    </View>
  );
}

Compound Components

Create components with sub-components for better composition:

typescript
import { View, Text, type ViewProps } from 'react-native';
import { createContext, useContext } from 'react';

// Context for sharing state
const CardContext = createContext<{ variant?: string }>({});

// Main component
export function Card({ children, variant = 'default', ...props }: ViewProps & { variant?: string }) {
  return (
    <CardContext.Provider value={{ variant }}>
      <View className="bg-white dark:bg-slate-800 rounded-xl overflow-hidden" {...props}>
        {children}
      </View>
    </CardContext.Provider>
  );
}

// Sub-components
Card.Header = function CardHeader({ children, ...props }: ViewProps) {
  return (
    <View className="p-6 border-b border-slate-200 dark:border-slate-700" {...props}>
      {children}
    </View>
  );
};

Card.Body = function CardBody({ children, ...props }: ViewProps) {
  return (
    <View className="p-6" {...props}>
      {children}
    </View>
  );
};

Card.Footer = function CardFooter({ children, ...props }: ViewProps) {
  return (
    <View className="p-6 border-t border-slate-200 dark:border-slate-700" {...props}>
      {children}
    </View>
  );
};

Card.Title = function CardTitle({ children, ...props }: ViewProps & { children: string }) {
  return (
    <Text className="text-xl font-bold text-slate-900 dark:text-white" {...props}>
      {children}
    </Text>
  );
};

Card.Description = function CardDescription({ children, ...props }: ViewProps & { children: string }) {
  return (
    <Text className="text-slate-600 dark:text-slate-400 mt-1" {...props}>
      {children}
    </Text>
  );
};

// Usage
<Card>
  <Card.Header>
    <Card.Title>Card Title</Card.Title>
    <Card.Description>This is a description</Card.Description>
  </Card.Header>
  <Card.Body>
    <Text>Card content goes here</Text>
  </Card.Body>
  <Card.Footer>
    <Button>Action</Button>
  </Card.Footer>
</Card>

Polymorphic Component

Create flexible components that can render different elements:

typescript
interface TextProps {
  variant?: 'h1' | 'h2' | 'h3' | 'body' | 'caption';
  children: React.ReactNode;
  className?: string;
}

export function Typography({ variant = 'body', children, className }: TextProps) {
  const styles = {
    h1: 'text-4xl font-bold text-slate-900 dark:text-white',
    h2: 'text-3xl font-bold text-slate-900 dark:text-white',
    h3: 'text-2xl font-semibold text-slate-900 dark:text-white',
    body: 'text-base text-slate-700 dark:text-slate-300',
    caption: 'text-sm text-slate-500 dark:text-slate-400',
  };

  return (
    <Text className={cn(styles[variant], className)}>
      {children}
    </Text>
  );
}

Animated Component

typescript
import Animated, {
  useAnimatedStyle,
  withSpring,
  withTiming,
} from 'react-native-reanimated';
import { useState } from 'react';

export function AnimatedButton({ children, onPress }: { children: string; onPress: () => void }) {
  const [isPressed, setIsPressed] = useState(false);

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [
      { scale: withSpring(isPressed ? 0.95 : 1) },
    ],
    opacity: withTiming(isPressed ? 0.8 : 1, { duration: 100 }),
  }));

  return (
    <Animated.View style={animatedStyle}>
      <Pressable
        onPressIn={() => setIsPressed(true)}
        onPressOut={() => setIsPressed(false)}
        onPress={onPress}
        className="bg-blue-500 rounded-lg px-6 py-3"
      >
        <Text className="text-white font-semibold text-center">
          {children}
        </Text>
      </Pressable>
    </Animated.View>
  );
}

Component with Ref

typescript
import { forwardRef, useImperativeHandle, useRef } from 'react';
import { TextInput, type TextInputProps } from 'react-native';

export interface InputRef {
  focus: () => void;
  blur: () => void;
  clear: () => void;
}

export const Input = forwardRef<InputRef, TextInputProps>((props, ref) => {
  const inputRef = useRef<TextInput>(null);

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current?.focus(),
    blur: () => inputRef.current?.blur(),
    clear: () => inputRef.current?.clear(),
  }));

  return (
    <TextInput
      ref={inputRef}
      className="bg-white/10 border border-white/20 rounded-lg px-4 py-3 text-white"
      {...props}
    />
  );
});

Input.displayName = 'Input';

// Usage
const inputRef = useRef<InputRef>(null);

<Input ref={inputRef} />
<Button onPress={() => inputRef.current?.focus()}>
  Focus Input
</Button>

Best Practices

✅ Single Responsibility

Each component should do one thing well

✅ Prop Types

Always define TypeScript interfaces for props

✅ Composition over Inheritance

Use composition patterns like compound components

✅ Accessibility

Add accessibility props like accessibilityLabel

✅ Performance

Use React.memo() for expensive components

Keep components small and focused. If a component does too much, split it into smaller pieces.

Next Steps