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.tsxBasic 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-authoritytypescript
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.