Back to catalog

react-navigation-patterns

React Navigation v7 setup and navigation patterns. Use when implementing navigation, tab bars, stack navigators, modals, or deep linking.

  • 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-navigation-patterns
description: React Navigation v7 setup and navigation patterns. Use when implementing navigation, tab bars, stack navigators, modals, or deep linking.
---

# React Navigation Patterns

This skill covers React Navigation v7 patterns for implementing iOS-native navigation in GalliGo Mobile.

## When to Use This Skill

- Setting up navigation structure
- Implementing tab bars and stack navigators
- Creating modal presentations
- Navigating programmatically
- Handling route parameters
- Configuring screen options

## Core Navigation Types

### Bottom Tab Navigator
For main app navigation (3-5 top-level screens).

```typescript
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
import { HomeIcon, ExploreIcon, FriendsIcon, ProfileIcon } from '@/components/icons'

const Tab = createBottomTabNavigator()

export const MainTabs = () => (
  <Tab.Navigator
    screenOptions={{
      tabBarActiveTintColor: '#007AFF', // iOS blue
      tabBarInactiveTintColor: '#8E8E93', // iOS gray
      tabBarStyle: {
        height: 65,
        paddingBottom: 8,
        paddingTop: 8,
      },
      tabBarLabelStyle: {
        fontSize: 10,
        fontWeight: '500',
      },
    }}
  >
    <Tab.Screen
      name="Home"
      component={HomeScreen}
      options={{
        tabBarIcon: ({ color, size }) => (
          <HomeIcon size={size} color={color} />
        ),
        tabBarLabel: 'Home',
      }}
    />
    <Tab.Screen
      name="Explore"
      component={ExploreScreen}
      options={{
        tabBarIcon: ({ color, size }) => (
          <ExploreIcon size={size} color={color} />
        ),
      }}
    />
    <Tab.Screen
      name="Friends"
      component={FriendsScreen}
      options={{
        tabBarIcon: ({ color, size }) => (
          <FriendsIcon size={size} color={color} />
        ),
      }}
    />
    <Tab.Screen
      name="Profile"
      component={ProfileScreen}
      options={{
        tabBarIcon: ({ color, size }) => (
          <ProfileIcon size={size} color={color} />
        ),
      }}
    />
  </Tab.Navigator>
)
```

**Best practices**:
- Max 5 tabs (iOS HIG guideline)
- Always show icons
- Keep labels short (1-2 words)
- Use semantic icon names

### Native Stack Navigator
For hierarchical navigation (drill-down flows).

```typescript
import { createNativeStackNavigator } from '@react-navigation/native-stack'

const Stack = createNativeStackNavigator()

export const HomeStack = () => (
  <Stack.Navigator
    screenOptions={{
      headerBackTitleVisible: false, // Hide "Back" text
      headerTintColor: '#007AFF', // iOS blue
      headerShadowVisible: false, // No shadow on header
    }}
  >
    <Stack.Screen
      name="HomeMain"
      component={HomeScreen}
      options={{
        title: 'Home',
        headerLargeTitle: true, // iOS large title style
      }}
    />
    <Stack.Screen
      name="PlaceDetails"
      component={PlaceDetailsScreen}
      options={({ route }) => ({
        title: route.params.placeName || 'Place',
        headerBackTitle: 'Back',
      })}
    />
    <Stack.Screen
      name="PlaceGallery"
      component={PlaceGalleryScreen}
      options={{
        headerTransparent: true, // For fullscreen image galleries
        headerTitle: '',
      }}
    />
  </Stack.Navigator>
)
```

## Nested Navigators

Tabs inside Stack (common pattern):

```typescript
import { NavigationContainer } from '@react-navigation/native'

const RootStack = createNativeStackNavigator()

export const RootNavigator = () => (
  <NavigationContainer>
    <RootStack.Navigator screenOptions={{ headerShown: false }}>
      {/* Main app with tabs */}
      <RootStack.Screen name="Main" component={MainTabs} />

      {/* Modal screens */}
      <RootStack.Group screenOptions={{ presentation: 'modal' }}>
        <RootStack.Screen
          name="CreatePlace"
          component={CreatePlaceScreen}
          options={{
            title: 'New Place',
            headerShown: true,
          }}
        />
        <RootStack.Screen
          name="Settings"
          component={SettingsScreen}
        />
      </RootStack.Group>
    </RootStack.Navigator>
  </NavigationContainer>
)
```

## Modal Presentation

### Standard Modal (Slides from Bottom)
```typescript
<Stack.Screen
  name="CreatePlace"
  component={CreatePlaceScreen}
  options={{
    presentation: 'modal', // Slide up from bottom
    headerLeft: () => (
      <Pressable onPress={() => navigation.goBack()}>
        <Text className="text-blue-500">Cancel</Text>
      </Pressable>
    ),
    gestureEnabled: true, // Allow swipe down to dismiss
  }}
/>
```

### Full Screen Modal
```typescript
<Stack.Screen
  name="Onboarding"
  component={OnboardingScreen}
  options={{
    presentation: 'fullScreenModal',
    gestureEnabled: false, // Prevent dismissal
    headerShown: false,
  }}
/>
```

### Transparent Modal (Overlay)
```typescript
<Stack.Screen
  name="ShareSheet"
  component={ShareSheetScreen}
  options={{
    presentation: 'transparentModal',
    headerShown: false,
    animation: 'fade',
  }}
/>
```

## Programmatic Navigation

### Using useNavigation Hook
```typescript
import { useNavigation } from '@react-navigation/native'
import type { NativeStackNavigationProp } from '@react-navigation/native-stack'
import type { RootStackParamList } from '@/types/navigation'

const Component = () => {
  const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()

  const goToDetails = () => {
    navigation.navigate('PlaceDetails', {
      placeId: '123',
      placeName: 'Cafe Tartine',
    })
  }

  const goBack = () => {
    navigation.goBack()
  }

  const goToRoot = () => {
    navigation.popToTop()
  }

  return (
    <Pressable onPress={goToDetails}>
      <Text>View Details</Text>
    </Pressable>
  )
}
```

### Navigation Methods
```typescript
// Navigate to screen
navigation.navigate('ScreenName', { param: 'value' })

// Go back
navigation.goBack()

// Go to top of stack
navigation.popToTop()

// Pop N screens
navigation.pop(2)

// Replace current screen
navigation.replace('NewScreen')

// Reset navigation state
navigation.reset({
  index: 0,
  routes: [{ name: 'Home' }],
})

// Push (even if screen exists)
navigation.push('ScreenName', { param: 'value' })
```

## Route Parameters

### TypeScript Param List
```typescript
// types/navigation.ts
export type RootStackParamList = {
  Home: undefined // No params
  PlaceDetails: {
    placeId: string
    placeName?: string
  }
  CreatePlace: {
    initialLocation?: {
      latitude: number
      longitude: number
    }
  }
  PlaceGallery: {
    placeId: string
    initialIndex?: number
  }
}
```

### Accessing Route Params
```typescript
import { useRoute } from '@react-navigation/native'
import type { RouteProp } from '@react-navigation/native'
import type { RootStackParamList } from '@/types/navigation'

type PlaceDetailsRouteProp = RouteProp<RootStackParamList, 'PlaceDetails'>

const PlaceDetailsScreen = () => {
  const route = useRoute<PlaceDetailsRouteProp>()
  const { placeId, placeName } = route.params

  // Fetch place data
  const { data: place } = useQuery({
    queryKey: ['place', placeId],
    queryFn: () => fetchPlace(placeId),
  })

  return (
    <View>
      <Text>{placeName || place?.name}</Text>
    </View>
  )
}
```

### Setting Params Dynamically
```typescript
const PlaceDetailsScreen = () => {
  const navigation = useNavigation()

  useEffect(() => {
    if (place) {
      // Update screen title when place loads
      navigation.setOptions({
        title: place.name,
      })
    }
  }, [place, navigation])

  return <View>{/* Content */}</View>
}
```

## Screen Options

### Dynamic Screen Options
```typescript
<Stack.Screen
  name="PlaceDetails"
  component={PlaceDetailsScreen}
  options={({ route, navigation }) => ({
    title: route.params.placeName || 'Place',
    headerRight: () => (
      <Pressable onPress={() => handleShare(route.params.placeId)}>
        <ShareIcon />
      </Pressable>
    ),
  })}
/>
```

### Common Screen Options
```typescript
options={{
  // Title
  title: 'Screen Title',
  headerLargeTitle: true, // iOS large title

  // Back button
  headerBackTitle: 'Back',
  headerBackTitleVisible: false,

  // Header styling
  headerTintColor: '#007AFF',
  headerStyle: {
    backgroundColor: '#fff',
  },
  headerShadowVisible: false,
  headerTransparent: true,

  // Header buttons
  headerLeft: () => <CancelButton />,
  headerRight: () => <ShareButton />,

  // Gestures
  gestureEnabled: true,
  fullScreenGestureEnabled: false,

  // Animation
  animation: 'default', // 'fade' | 'slide_from_right' | 'slide_from_bottom'
  presentation: 'card', // 'modal' | 'transparentModal' | 'fullScreenModal'

  // Tab bar (for tab screens)
  tabBarIcon: ({ color, size }) => <Icon color={color} size={size} />,
  tabBarLabel: 'Label',
  tabBarBadge: 3, // Red badge with count
}}
```

## Navigation Lifecycle

### useFocusEffect Hook
Run code when screen comes into focus:

```typescript
import { useFocusEffect } from '@react-navigation/native'
import { useCallback } from 'react'

const HomeScreen = () => {
  useFocusEffect(
    useCallback(() => {
      // Runs when screen comes into focus
      console.log('Screen focused')

      // Cleanup when screen loses focus
      return () => {
        console.log('Screen blurred')
      }
    }, [])
  )

  return <View>{/* Content */}</View>
}
```

**Use cases**:
- Refresh data when returning to screen
- Resume animations
- Start tracking analytics
- Re-enable subscriptions

### Navigation Events
```typescript
import { useNavigation } from '@react-navigation/native'
import { useEffect } from 'react'

const Component = () => {
  const navigation = useNavigation()

  useEffect(() => {
    const unsubscribeFocus = navigation.addListener('focus', () => {
      console.log('Screen focused')
    })

    const unsubscribeBlur = navigation.addListener('blur', () => {
      console.log('Screen blurred')
    })

    return () => {
      unsubscribeFocus()
      unsubscribeBlur()
    }
  }, [navigation])

  return <View>{/* Content */}</View>
}
```

## Header Customization

### Custom Header Component
```typescript
const CustomHeader = ({ title, onBack }) => (
  <View className="flex-row items-center justify-between px-4 py-3 bg-white border-b border-gray-200">
    <Pressable onPress={onBack}>
      <ChevronLeftIcon />
    </Pressable>
    <Text className="text-lg font-semibold">{title}</Text>
    <View className="w-6" /> {/* Spacer for centering */}
  </View>
)

<Stack.Screen
  name="PlaceDetails"
  component={PlaceDetailsScreen}
  options={{
    header: ({ navigation, route }) => (
      <CustomHeader
        title={route.params.placeName}
        onBack={navigation.goBack}
      />
    ),
  }}
/>
```

### Hide Header on Scroll
```typescript
const PlaceDetailsScreen = () => {
  const navigation = useNavigation()

  const handleScroll = (event) => {
    const offsetY = event.nativeEvent.contentOffset.y
    navigation.setOptions({
      headerTransparent: offsetY > 200,
    })
  }

  return (
    <ScrollView onScroll={handleScroll} scrollEventThrottle={16}>
      {/* Content */}
    </ScrollView>
  )
}
```

## Deep Linking

### Configure Linking
```typescript
// App.tsx or navigation setup
const linking = {
  prefixes: ['galligo://', 'https://galligo.app'],
  config: {
    screens: {
      Home: 'home',
      PlaceDetails: 'place/:placeId',
      CreatePlace: 'create',
    },
  },
}

<NavigationContainer linking={linking}>
  {/* Navigation */}
</NavigationContainer>
```

**URL Examples**:
- `galligo://home` → Home screen
- `galligo://place/123` → PlaceDetails with `placeId: '123'`
- `https://galligo.app/place/456` → PlaceDetails with `placeId: '456'`

## Navigation Guards

### Prevent Going Back
```typescript
import { useEffect } from 'react'
import { useNavigation } from '@react-navigation/native'

const OnboardingScreen = () => {
  const navigation = useNavigation()

  useEffect(() => {
    // Prevent back navigation
    const unsubscribe = navigation.addListener('beforeRemove', (e) => {
      e.preventDefault()
      // Optionally show confirmation
    })

    return unsubscribe
  }, [navigation])

  return <View>{/* Content */}</View>
}
```

### Confirm Before Leaving (Unsaved Changes)
```typescript
const CreatePlaceScreen = () => {
  const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
  const navigation = useNavigation()

  useEffect(() => {
    if (!hasUnsavedChanges) return

    const unsubscribe = navigation.addListener('beforeRemove', (e) => {
      if (!hasUnsavedChanges) return

      e.preventDefault()

      Alert.alert(
        'Discard changes?',
        'You have unsaved changes. Are you sure you want to leave?',
        [
          { text: 'Cancel', style: 'cancel' },
          {
            text: 'Discard',
            style: 'destructive',
            onPress: () => navigation.dispatch(e.data.action),
          },
        ]
      )
    })

    return unsubscribe
  }, [navigation, hasUnsavedChanges])

  return <View>{/* Form */}</View>
}
```

## Best Practices

### 1. Type-Safe Navigation
Always define param lists and use TypeScript:

```typescript
// ✅ GOOD: Type-safe
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()
navigation.navigate('PlaceDetails', { placeId: '123' }) // TypeScript checks params

// ❌ BAD: No types
const navigation = useNavigation()
navigation.navigate('PlaceDetails', { id: 123 }) // Wrong param name, no error
```

### 2. Keep Tab Count Reasonable
```typescript
// ✅ GOOD: 4 tabs
<Tab.Navigator>
  <Tab.Screen name="Home" />
  <Tab.Screen name="Explore" />
  <Tab.Screen name="Friends" />
  <Tab.Screen name="Profile" />
</Tab.Navigator>

// ❌ BAD: 6 tabs (too many)
<Tab.Navigator>
  <Tab.Screen name="Tab1" />
  {/* ... 6 tabs total - against iOS HIG */}
</Tab.Navigator>
```

### 3. Use Modals for Creation Flows
```typescript
// ✅ GOOD: Modal for creation
<Stack.Screen
  name="CreatePlace"
  options={{ presentation: 'modal' }}
/>

// ❌ BAD: Regular push for creation
<Stack.Screen name="CreatePlace" />
```

### 4. Cleanup on Blur
```typescript
// ✅ GOOD: Cleanup when leaving screen
useFocusEffect(
  useCallback(() => {
    const subscription = subscribeToUpdates()
    return () => subscription.unsubscribe() // Cleanup
  }, [])
)
```

## Related Skills

- @ios-design-guidelines - iOS navigation patterns and standards
- @react-native-patterns - Component implementation
- @mobile-accessibility - Accessible navigation

---

React Navigation provides iOS-native navigation patterns. Follow iOS HIG for tab counts, modal usage, and gesture handling.