Navigation System
Navigation System
Section titled “Navigation System”Problem and Rationale
Section titled “Problem and Rationale”Mobile navigation requires handling:
- Multiple navigation patterns (tabs, stacks, modals)
- Deep linking from push notifications and external URLs
- Authentication-based navigation guards
- State preservation across navigation
- Platform-specific navigation behaviors (iOS/Android)
Why React Navigation:
- Native platform integration (iOS/Android navigation patterns)
- Type-safe navigation with TypeScript
- Deep linking and URL scheme support
- Customizable transitions and animations
- Extensive community support and documentation
Design
Section titled “Design”Navigation Hierarchy
Section titled “Navigation Hierarchy”AppNavigator (Root) ↓BottomSheetProvider → KeyboardProvider → RefsProvider ↓NavigationContainer (with deep linking) ↓Auth Check ↓├─ Logged In: Stack Navigator (ChatScreen, ContactDetails, Dashboard)│ ↓│ Tab Navigator│ ├─ Inbox Tab → InboxStack│ ├─ Conversations Tab → ConversationStack│ └─ Settings Tab → SettingsStack│└─ Logged Out: AuthStack ├─ Login ├─ ConfigURL └─ ForgotPasswordRoute Type Definitions
Section titled “Route Type Definitions”Tab Routes (src/navigation/tabs/AppTabs.tsx):
export type TabParamList = { Conversations: undefined; Inbox: undefined; Settings: undefined; Login: undefined; ConfigInstallationURL: undefined; ForgotPassword: undefined; Search: undefined; Notifications: undefined;};Stack Routes (Overlays):
export type TabBarExcludedScreenParamList = { Tab: undefined; ChatScreen: { conversationId: number; primaryActorId?: number; primaryActorType?: string; }; ContactDetails: { conversationId: number }; ConversationActions: undefined; Dashboard: { url: string }; Login: undefined; SearchScreen: undefined; ImageScreen: undefined;};Deep Linking Configuration
Section titled “Deep Linking Configuration”URL Structure:
{installationUrl}/app/accounts/{accountId}/conversations/{conversationId}/{primaryActorId?}/{primaryActorType?}Configuration (src/navigation/index.tsx):
const linking = { prefixes: [installationUrl], // From Redux state config: { screens: { ChatScreen: { path: 'app/accounts/:accountId/conversations/:conversationId/:primaryActorId?/:primaryActorType?', parse: { conversationId: (id: string) => parseInt(id), primaryActorId: (id: string) => parseInt(id), primaryActorType: (type: string) => decodeURIComponent(type), }, }, }, }, getStateFromPath, // Custom path parsing getInitialURL, // Handle deep links and push notifications subscribe, // Listen for incoming links};Lifecycle and Data Flow
Section titled “Lifecycle and Data Flow”App Launch Navigation Flow
Section titled “App Launch Navigation Flow”App Start ↓Load Fonts & Assets ↓Check Auth State (Redux) ↓├─ Logged In│ ↓│ Initialize User Data│ ↓│ Setup WebSocket│ ↓│ Navigate to Tab Navigator (Inbox Tab)│└─ Logged Out ↓ Navigate to AuthStack (Login)Deep Link Navigation Flow
Section titled “Deep Link Navigation Flow”Deep Link Received (URL or Push Notification) ↓Parse URL → Extract conversationId, accountId ↓Check Authentication ↓├─ Authenticated│ ↓│ Navigate to ChatScreen with params│ ↓│ Load Conversation Data│└─ Not Authenticated ↓ Navigate to Login ↓ Store Deep Link Intent ↓ After Login → Navigate to Intended ScreenPush Notification Flow
Section titled “Push Notification Flow”App Closed/Background → Notification Received ↓User Taps Notification ↓getInitialNotification() or onNotificationOpenedApp() ↓Extract FCM Message Data ↓Transform to Conversation Link ↓Navigate via Deep Link SystemImplementation Notes
Section titled “Implementation Notes”Navigation Container Setup
Section titled “Navigation Container Setup”Main Navigation Container (src/navigation/index.tsx):
export const AppNavigationContainer = () => { const [fontsLoaded] = useFonts({ 'Inter-400-20': require('../assets/fonts/Inter-400-20.ttf'), // ... other fonts });
const installationUrl = useAppSelector(selectInstallationUrl); const locale = useAppSelector(selectLocale);
const linking = { prefixes: [installationUrl], config: { /* ... */ }, getInitialURL: async () => { // Check deep link const url = await Linking.getInitialURL(); if (url) return url;
// Check push notification const message = await messaging().getInitialNotification(); if (message) { return extractConversationLink(message); } }, subscribe: (listener) => { // Listen for deep links const linkSubscription = Linking.addEventListener('url', ({ url }) => listener(url) );
// Listen for push notifications const notifSubscription = messaging().onNotificationOpenedApp(message => { const link = extractConversationLink(message); if (link) listener(link); });
return () => { linkSubscription.remove(); notifSubscription(); }; }, };
return ( <NavigationContainer linking={linking} ref={navigationRef} fallback={<ActivityIndicator />}> <AppTabs /> </NavigationContainer> );};Auth-Based Navigation
Section titled “Auth-Based Navigation”Conditional Rendering (src/navigation/tabs/AppTabs.tsx):
export const AppTabs = () => { const isLoggedIn = useAppSelector(selectLoggedIn);
if (isLoggedIn) { return ( <Stack.Navigator screenOptions={{ headerShown: false }}> <Stack.Screen name="Tab" component={Tabs} /> <Stack.Screen name="ChatScreen" component={ChatScreen} options={{ animation: 'slide_from_right' }} /> <Stack.Screen name="ContactDetails" component={ContactDetailsScreen} options={{ presentation: 'formSheet', animation: 'slide_from_bottom', }} /> <Stack.Screen name="Dashboard" component={DashboardScreen} options={{ presentation: 'formSheet', animation: 'slide_from_bottom', }} /> </Stack.Navigator> ); } else { return <AuthStack />; }};Permission-Based Tab Visibility
Section titled “Permission-Based Tab Visibility”const Tabs = () => { const user = useAppSelector(selectUser); const userPermissions = user ? getUserPermissions(user, user.account_id) : [];
const hasConversationPermission = CONVERSATION_PERMISSIONS.some(permission => userPermissions.includes(permission) );
return ( <Tab.Navigator tabBar={CustomTabBar} initialRouteName="Inbox"> {hasConversationPermission && ( <Tab.Screen name="Inbox" component={InboxStack} /> )} {hasConversationPermission && ( <Tab.Screen name="Conversations" component={ConversationStack} /> )} <Tab.Screen name="Settings" component={SettingsStack} /> </Tab.Navigator> );};Programmatic Navigation
Section titled “Programmatic Navigation”Navigation Utilities (src/utils/navigationUtils.ts):
import { createNavigationContainerRef } from '@react-navigation/native';
export const navigationRef = createNavigationContainerRef();
export function navigate(name: string, params?: object) { if (navigationRef.isReady()) { navigationRef.navigate(name as never, params as never); }}
export function goBack() { if (navigationRef.isReady() && navigationRef.canGoBack()) { navigationRef.goBack(); }}
export function getCurrentRoute() { if (navigationRef.isReady()) { return navigationRef.getCurrentRoute(); }}Usage in Components:
import { navigate } from '@/utils/navigationUtils';
const handleConversationPress = (conversationId: number) => { navigate('ChatScreen', { conversationId });};Screen Transitions
Section titled “Screen Transitions”Platform-Specific Animations:
<Stack.Screen name="ChatScreen" component={ChatScreen} options={{ animation: 'slide_from_right', // iOS-style slide headerShown: false, }}/>
<Stack.Screen name="ContactDetails" component={ContactDetailsScreen} options={{ presentation: 'formSheet', // iOS modal presentation animation: 'slide_from_bottom', }}/>Patterns and Anti-Patterns
Section titled “Patterns and Anti-Patterns”✅ Good Patterns:
// Use typed navigationimport { useNavigation } from '@react-navigation/native';import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
type NavigationProp = NativeStackNavigationProp<TabBarExcludedScreenParamList>;
const MyComponent = () => { const navigation = useNavigation<NavigationProp>();
navigation.navigate('ChatScreen', { conversationId: 123 });};
// Use navigation guardsuseEffect(() => { if (!isLoggedIn) { navigation.replace('Login'); }}, [isLoggedIn]);❌ Anti-Patterns:
// DON'T: Use strings without type checkingnavigation.navigate('NonExistentScreen'); // ❌ No compile-time error
// DON'T: Navigate before NavigationContainer is ready// Always check navigationRef.isReady()
// DON'T: Store navigation state in Redux// Let React Navigation manage its own stateTesting Strategy
Section titled “Testing Strategy”Navigation Tests
Section titled “Navigation Tests”describe('Navigation', () => { it('should navigate to ChatScreen with conversationId', () => { const { getByTestId } = render(<AppNavigator />);
fireEvent.press(getByTestId('conversation-item-1'));
expect(mockNavigation.navigate).toHaveBeenCalledWith( 'ChatScreen', { conversationId: 1 } ); });
it('should redirect to login if not authenticated', () => { mockUseAppSelector.mockReturnValue(false); // Not logged in
render(<AppNavigator />);
expect(screen.getByTestId('login-screen')).toBeTruthy(); });});Deep Link Tests
Section titled “Deep Link Tests”describe('Deep Linking', () => { it('should parse conversation URL correctly', async () => { const url = 'https://app.Zelta Chat.com/app/accounts/1/conversations/123';
const state = linking.getStateFromPath(url, linking.config);
expect(state.routes[0]).toMatchObject({ name: 'ChatScreen', params: { conversationId: 123 }, }); });});Performance Considerations
Section titled “Performance Considerations”Lazy Screen Loading
Section titled “Lazy Screen Loading”Screens are loaded on-demand:
const ChatScreen = lazy(() => import('@/screens/chat-screen/ChatScreen'));Screen Options Optimization
Section titled “Screen Options Optimization”Memoize screen options to prevent re-renders:
const screenOptions = useMemo(() => ({ headerShown: false, animation: 'slide_from_right',}), []);Navigation State Persistence
Section titled “Navigation State Persistence”React Navigation handles state persistence automatically. For custom persistence:
const [isReady, setIsReady] = useState(false);const [initialState, setInitialState] = useState();
useEffect(() => { const restoreState = async () => { const savedState = await getNavigationState(); if (savedState) setInitialState(savedState); setIsReady(true); };
restoreState();}, []);
<NavigationContainer initialState={initialState} onStateChange={saveNavigationState}/>Extending This Concept
Section titled “Extending This Concept”Adding a New Screen
Section titled “Adding a New Screen”- Define route params:
export type TabParamList = { // ... existing routes NewScreen: { param1: string; param2?: number };};- Add screen to navigator:
<Stack.Screen name="NewScreen" component={NewScreenComponent} options={{ headerShown: true, title: 'New Screen', }}/>- Navigate to screen:
navigation.navigate('NewScreen', { param1: 'value', param2: 123});Adding Deep Link Support
Section titled “Adding Deep Link Support”const linking = { config: { screens: { // ... existing screens NewScreen: { path: 'new-feature/:id', parse: { id: (id: string) => parseInt(id), }, }, }, },};Custom Tab Bar
Section titled “Custom Tab Bar”Create custom tab bar component:
const CustomTabBar = (props: BottomTabBarProps) => { return ( <View style={styles.tabBar}> {props.state.routes.map((route, index) => ( <TouchableOpacity key={route.key} onPress={() => props.navigation.navigate(route.name)}> <Text>{route.name}</Text> </TouchableOpacity> ))} </View> );};
<Tab.Navigator tabBar={CustomTabBar}> {/* tabs */}</Tab.Navigator>Further Reading
Section titled “Further Reading”- Authentication Flow - Auth-based navigation
- Push Notifications - Notification-driven navigation
- Conversations Feature - Conversation navigation
- React Navigation Documentation