Back to catalog

react-native-patterns

React Native component patterns and best practices. Use when building RN components, handling platform differences, or implementing common mobile UI patterns.

  • Category: discovered
  • Tags: github, community
  • Version: 1.0.0
  • Author: walid
  • Downloads: 0
  • Updated: 2025-11-22

Download the latest version (v1.0.0) from this page.

SKILL.md

---
name: react-native-patterns
description: React Native component patterns and best practices. Use when building RN components, handling platform differences, or implementing common mobile UI patterns.
---

# React Native Patterns

This skill provides common React Native component patterns and best practices for building mobile UI in the GalliGo app.

## When to Use This Skill

- Building React Native components
- Handling iOS/Android platform differences
- Implementing common mobile UI patterns (cards, lists, buttons)
- Styling with StyleSheet and NativeWind
- Managing component state and props

## Core Component Patterns

### Button with Pressed State
```typescript
import { Pressable, Text } from 'react-native'

const Button = ({ onPress, children }) => (
  <Pressable
    onPress={onPress}
    className={({ pressed }) =>
      `p-4 rounded-lg ${pressed ? 'bg-blue-600' : 'bg-blue-500'}`
    }
  >
    <Text className="text-white font-semibold text-center">
      {children}
    </Text>
  </Pressable>
)
```

**Key points**:
- Use `Pressable` instead of `TouchableOpacity` for better control
- `className` can accept a function to handle pressed state
- Always wrap text in `<Text>` component

### Safe Area Wrapper
```typescript
import { SafeAreaView } from 'react-native-safe-area-context'
import { View } from 'react-native'

// Screen component with safe area
export const Screen = ({ children }) => (
  <SafeAreaView className="flex-1 bg-white">
    {children}
  </SafeAreaView>
)

// Or use SafeAreaView edges for specific sides
export const ModalScreen = ({ children }) => (
  <SafeAreaView edges={['top', 'left', 'right']} className="flex-1 bg-white">
    {/* Let modal handle bottom edge */}
    {children}
  </SafeAreaView>
)
```

**Key points**:
- Use `react-native-safe-area-context` (not built-in SafeAreaView)
- Apply to all screen-level components
- Control which edges with `edges` prop
- Combine with `flex-1` for full-height layouts

### Card Component with Shadow
```typescript
import { View, Text, Platform, StyleSheet } from 'react-native'

const Card = ({ title, description, children }) => (
  <View className="bg-white rounded-xl p-4" style={styles.shadow}>
    {title && <Text className="text-lg font-semibold mb-2">{title}</Text>}
    {description && <Text className="text-gray-600 mb-3">{description}</Text>}
    {children}
  </View>
)

const styles = StyleSheet.create({
  shadow: Platform.select({
    ios: {
      shadowColor: '#000',
      shadowOffset: { width: 0, height: 2 },
      shadowOpacity: 0.1,
      shadowRadius: 8,
    },
    android: {
      elevation: 4,
    },
  }),
})

export default Card
```

**Key points**:
- Use `Platform.select()` for iOS shadows vs Android elevation
- Combine NativeWind (layout/colors) with StyleSheet (shadows)
- iOS shadows need 4 properties: color, offset, opacity, radius

### List Item with Touch Target
```typescript
import { Pressable, View, Text, Image } from 'react-native'

const ListItem = ({ item, onPress }) => (
  <Pressable
    onPress={onPress}
    className="flex-row items-center p-4 border-b border-gray-200"
    accessible={true}
    accessibilityLabel={`${item.title} - ${item.subtitle}`}
    accessibilityRole="button"
  >
    <Image
      source={{ uri: item.imageUrl }}
      className="w-12 h-12 rounded-full mr-3"
    />
    <View className="flex-1">
      <Text className="text-base font-medium text-gray-900">
        {item.title}
      </Text>
      <Text className="text-sm text-gray-600">
        {item.subtitle}
      </Text>
    </View>
    <ChevronRightIcon />
  </Pressable>
)
```

**Key points**:
- Minimum 44pt height for touch target
- Add `accessible` and `accessibilityLabel` for VoiceOver
- Use `flex-row` for horizontal layout
- Add visual feedback with Pressable's pressed state

## Platform-Specific Patterns

### Platform.select() for Different Behaviors
```typescript
import { Platform } from 'react-native'

// Different values by platform
const spacing = Platform.select({
  ios: 16,
  android: 12,
  default: 16,
})

// Different components by platform
const HeaderIcon = Platform.select({
  ios: () => <IOSIcon />,
  android: () => <AndroidIcon />,
})

// In StyleSheet
const styles = StyleSheet.create({
  container: {
    paddingTop: Platform.select({
      ios: 20,
      android: 16,
    }),
  },
})
```

### Platform.OS for Conditional Logic
```typescript
import { Platform } from 'react-native'

// Conditional rendering
{Platform.OS === 'ios' && <IOSOnlyFeature />}

// Conditional logic
const handleShare = () => {
  if (Platform.OS === 'ios') {
    // iOS share implementation
  } else {
    // Android share implementation
  }
}

// Version check
if (Platform.Version >= 15) {
  // iOS 15+ specific feature
}
```

## Layout Patterns

### Flexbox Layouts (Default in RN)
```typescript
// Vertical layout (default)
<View className="flex-1">
  <View className="h-20 bg-blue-500" /> {/* Header */}
  <View className="flex-1 bg-white" /> {/* Content */}
  <View className="h-16 bg-gray-100" /> {/* Footer */}
</View>

// Horizontal layout
<View className="flex-row items-center justify-between p-4">
  <Text>Left</Text>
  <Text>Right</Text>
</View>

// Centered content
<View className="flex-1 items-center justify-center">
  <Text>Centered</Text>
</View>

// Space between items (use margin/padding, not gap)
<View className="flex-row">
  <View className="mr-2">Item 1</View>
  <View className="mr-2">Item 2</View>
  <View>Item 3</View>
</View>
```

**Key points**:
- `flexDirection: 'column'` is default (vertical)
- Use `flex-row` for horizontal layouts
- No `gap` property in older RN (use margin/padding)
- `flex-1` expands to fill available space

### Absolute Positioning for Overlays
```typescript
// Overlay with absolute positioning
<View className="relative">
  {/* Base content */}
  <Image source={...} className="w-full h-64" />

  {/* Overlay badge */}
  <View className="absolute top-2 right-2 bg-red-500 rounded-full px-2 py-1">
    <Text className="text-white text-xs">New</Text>
  </View>

  {/* Bottom gradient overlay */}
  <View className="absolute bottom-0 left-0 right-0 h-20 bg-gradient-to-t from-black/50" />
</View>
```

### ScrollView with Keyboard Dismiss
```typescript
import { ScrollView, KeyboardAvoidingView, Platform } from 'react-native'

const FormScreen = () => (
  <KeyboardAvoidingView
    behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
    className="flex-1"
  >
    <ScrollView
      className="flex-1"
      keyboardDismissMode="on-drag"
      keyboardShouldPersistTaps="handled"
    >
      {/* Form inputs */}
    </ScrollView>
  </KeyboardAvoidingView>
)
```

## Styling Patterns

### StyleSheet.create() at Component Bottom
```typescript
import { View, Text, StyleSheet } from 'react-native'

const Component = () => (
  <View style={styles.container}>
    <Text style={styles.title}>Title</Text>
  </View>
)

// StyleSheet at the bottom
const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 16,
    backgroundColor: '#fff',
  },
  title: {
    fontSize: 24,
    fontWeight: '700',
    color: '#111',
  },
})
```

### Combining StyleSheet with NativeWind
```typescript
// NativeWind for simple styles, StyleSheet for complex
<View className="p-4 bg-white rounded-xl" style={styles.shadow}>
  <Text className="text-lg font-semibold" style={styles.customFont}>
    Heading
  </Text>
</View>

const styles = StyleSheet.create({
  shadow: {
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 4 },
    shadowOpacity: 0.15,
    shadowRadius: 12,
  },
  customFont: {
    fontFamily: 'CustomFont-Bold', // Not available in NativeWind
  },
})
```

### Dynamic Styles Based on State
```typescript
const [isActive, setIsActive] = useState(false)

// Conditional className
<View className={isActive ? 'bg-blue-500' : 'bg-gray-300'}>
  <Text className={isActive ? 'text-white' : 'text-gray-700'}>
    {isActive ? 'Active' : 'Inactive'}
  </Text>
</View>

// Or with template literals
<View className={`p-4 rounded-lg ${isActive ? 'bg-blue-500' : 'bg-gray-300'}`}>
  {/* Content */}
</View>

// Or with StyleSheet array syntax
<View style={[styles.base, isActive && styles.active]}>
  {/* Content */}
</View>
```

## Performance Patterns

### FlatList for Large Lists
```typescript
import { FlatList } from 'react-native'

const ItemsList = ({ items }) => (
  <FlatList
    data={items}
    renderItem={({ item }) => <ItemCard item={item} />}
    keyExtractor={(item) => item.id}

    // Performance optimizations
    removeClippedSubviews={true}
    maxToRenderPerBatch={10}
    windowSize={5}

    // Pull to refresh
    refreshing={isRefreshing}
    onRefresh={handleRefresh}

    // Infinite scroll
    onEndReached={loadMore}
    onEndReachedThreshold={0.5}

    // Styling
    contentContainerClassName="p-4"
    ItemSeparatorComponent={() => <View className="h-3" />}
  />
)
```

**Key points**:
- Use FlatList instead of ScrollView + map for lists
- Set `keyExtractor` for stable keys
- Use performance props for large lists
- `contentContainerClassName` for padding around list

### Memoization for Expensive Components
```typescript
import { memo } from 'react'

// Memoize component to prevent unnecessary re-renders
const PlaceCard = memo(({ place, onPress }) => (
  <Pressable onPress={onPress}>
    {/* Card content */}
  </Pressable>
))

// Or use React.memo with custom comparison
const PlaceCard = memo(
  ({ place, onPress }) => (
    <Pressable onPress={onPress}>
      {/* Card content */}
    </Pressable>
  ),
  (prevProps, nextProps) => prevProps.place.id === nextProps.place.id
)
```

## Image Handling

### Image with Fallback
```typescript
import { Image } from 'react-native'
import { useState } from 'react'

const PlaceImage = ({ uri, placeholder }) => {
  const [error, setError] = useState(false)

  return (
    <Image
      source={error ? placeholder : { uri }}
      onError={() => setError(true)}
      className="w-full h-48 rounded-lg"
      resizeMode="cover"
    />
  )
}
```

### Image with Loading State
```typescript
import { Image, ActivityIndicator, View } from 'react-native'
import { useState } from 'react'

const PlaceImage = ({ uri }) => {
  const [loading, setLoading] = useState(true)

  return (
    <View className="relative">
      <Image
        source={{ uri }}
        onLoadStart={() => setLoading(true)}
        onLoadEnd={() => setLoading(false)}
        className="w-full h-48 rounded-lg"
      />
      {loading && (
        <View className="absolute inset-0 items-center justify-center bg-gray-100">
          <ActivityIndicator />
        </View>
      )}
    </View>
  )
}
```

## Common Gotchas

### Text Must Be Wrapped in <Text>
```typescript
// ❌ WRONG: Raw text not allowed
<View>Hello World</View>

// ✅ CORRECT: Wrap in Text
<View>
  <Text>Hello World</Text>
</View>
```

### No Default Inheritance of Text Styles
```typescript
// ❌ WRONG: Nested text doesn't inherit styles
<Text className="text-blue-500">
  Hello <Text>World</Text> {/* World won't be blue */}
</Text>

// ✅ CORRECT: Style each Text separately or use nesting
<Text className="text-blue-500">
  Hello <Text className="text-blue-500">World</Text>
</Text>

// Or use style prop which does inherit
<Text style={{ color: 'blue' }}>
  Hello <Text>World</Text> {/* Inherits color */}
</Text>
```

### Percentage Heights Require Parent Dimensions
```typescript
// ❌ WRONG: Percentage won't work without parent height
<View>
  <View className="h-1/2"> {/* Won't work */}
    <Text>Half height?</Text>
  </View>
</View>

// ✅ CORRECT: Parent has defined height
<View className="h-screen">
  <View className="h-1/2"> {/* Works */}
    <Text>Half screen height</Text>
  </View>
</View>

// Or use flex
<View className="flex-1">
  <View className="flex-1"> {/* Takes half */}
    <Text>Top half</Text>
  </View>
  <View className="flex-1"> {/* Takes other half */}
    <Text>Bottom half</Text>
  </View>
</View>
```

## Related Skills

- @ios-design-guidelines - iOS HIG compliance
- @nativewind-styling - NativeWind utilities
- @mobile-accessibility - Accessible component patterns
- @reanimated-library - Adding animations to components

---

Use these patterns as building blocks for GalliGo Mobile components. Prefer composition over inheritance, and always test on physical iOS devices.