Navigation Architecture
Rippler uses React Navigation v6 with a nested navigator structure:
This nested structure provides tab-based navigation on the main screen with the ability to push full-screen modals (like WorkoutScreen) on top.
Navigator Hierarchy
Root Stack Navigator
The top-level navigator manages the core app flow:
client/navigation/RootStackNavigator.tsx
import React from "react";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import MainTabNavigator from "@/navigation/MainTabNavigator";
import WorkoutScreen from "@/screens/WorkoutScreen";
import { useScreenOptions } from "@/hooks/useScreenOptions";
export type RootStackParamList = {
Main: undefined;
Workout: { week: number; day: string };
};
const Stack = createNativeStackNavigator<RootStackParamList>();
export default function RootStackNavigator() {
const screenOptions = useScreenOptions();
return (
<Stack.Navigator screenOptions={screenOptions}>
<Stack.Screen
name="Main"
component={MainTabNavigator}
options={{ headerShown: false }}
/>
<Stack.Screen
name="Workout"
component={WorkoutScreen}
options={{
headerTitle: "Log Workout",
}}
/>
</Stack.Navigator>
);
}
The main tab navigator (no params required)
Full-screen workout logging modal
Current program week (1-12)
Day of the week (e.g., “Monday”, “Tuesday”)
Main Tab Navigator
The bottom tab bar provides access to the four main sections:
client/navigation/MainTabNavigator.tsx
import React from "react";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { Feather } from "@expo/vector-icons";
import { BlurView } from "expo-blur";
import { Platform, StyleSheet } from "react-native";
import ProgramStackNavigator from "@/navigation/ProgramStackNavigator";
import HistoryStackNavigator from "@/navigation/HistoryStackNavigator";
import ExercisesStackNavigator from "@/navigation/ExercisesStackNavigator";
import GoalsStackNavigator from "@/navigation/GoalsStackNavigator";
import { useTheme } from "@/hooks/useTheme";
export type MainTabParamList = {
ProgramTab: undefined;
GoalsTab: undefined;
HistoryTab: undefined;
ExercisesTab: undefined;
};
const Tab = createBottomTabNavigator<MainTabParamList>();
export default function MainTabNavigator() {
const { theme, isDark } = useTheme();
return (
<Tab.Navigator
initialRouteName="ProgramTab"
screenOptions={{
tabBarActiveTintColor: theme.tabIconSelected,
tabBarInactiveTintColor: theme.tabIconDefault,
tabBarStyle: {
position: "absolute",
backgroundColor: Platform.select({
ios: "transparent",
android: theme.backgroundRoot,
}),
borderTopWidth: 0,
elevation: 0,
},
tabBarBackground: () =>
Platform.OS === "ios" ? (
<BlurView
intensity={100}
tint={isDark ? "dark" : "light"}
style={StyleSheet.absoluteFill}
/>
) : null,
headerShown: false,
}}
>
<Tab.Screen
name="ProgramTab"
component={ProgramStackNavigator}
options={{
title: "Program",
tabBarIcon: ({ color, size }) => (
<Feather name="calendar" size={size} color={color} />
),
}}
/>
<Tab.Screen
name="GoalsTab"
component={GoalsStackNavigator}
options={{
title: "Goals",
tabBarIcon: ({ color, size }) => (
<Feather name="target" size={size} color={color} />
),
}}
/>
<Tab.Screen
name="HistoryTab"
component={HistoryStackNavigator}
options={{
title: "History",
tabBarIcon: ({ color, size }) => (
<Feather name="bar-chart-2" size={size} color={color} />
),
}}
/>
<Tab.Screen
name="ExercisesTab"
component={ExercisesStackNavigator}
options={{
title: "Exercises",
tabBarIcon: ({ color, size }) => (
<Feather name="list" size={size} color={color} />
),
}}
/>
</Tab.Navigator>
);
}
Tab Screens
Program
Goals
History
Exercises
ProgramTab shows the workout program calendar:client/navigation/ProgramStackNavigator.tsx
import React from "react";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import ProgramScreen from "@/screens/ProgramScreen";
import { HeaderTitle } from "@/components/HeaderTitle";
import { useScreenOptions } from "@/hooks/useScreenOptions";
export type ProgramStackParamList = {
Program: undefined;
};
const Stack = createNativeStackNavigator<ProgramStackParamList>();
export default function ProgramStackNavigator() {
const screenOptions = useScreenOptions();
return (
<Stack.Navigator screenOptions={screenOptions}>
<Stack.Screen
name="Program"
component={ProgramScreen}
options={{
headerTitle: () => <HeaderTitle title="Rippler" />,
}}
/>
</Stack.Navigator>
);
}
Uses custom HeaderTitle component for branding
GoalsTab tracks target weights and progression:client/navigation/GoalsStackNavigator.tsx
export type GoalsStackParamList = {
Goals: undefined;
};
HistoryTab displays workout history and stats:client/navigation/HistoryStackNavigator.tsx
import React from "react";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import HistoryScreen from "@/screens/HistoryScreen";
import { useScreenOptions } from "@/hooks/useScreenOptions";
export type HistoryStackParamList = {
History: undefined;
};
const Stack = createNativeStackNavigator<HistoryStackParamList>();
export default function HistoryStackNavigator() {
const screenOptions = useScreenOptions();
return (
<Stack.Navigator screenOptions={screenOptions}>
<Stack.Screen
name="History"
component={HistoryScreen}
options={{
headerTitle: "History",
}}
/>
</Stack.Navigator>
);
}
ExercisesTab manages exercise library:client/navigation/ExercisesStackNavigator.tsx
export type ExercisesStackParamList = {
Exercises: undefined;
};
Screen Options Hook
Centralized screen configuration using a custom hook:
client/hooks/useScreenOptions.ts
import { Platform } from "react-native";
import { NativeStackNavigationOptions } from "@react-navigation/native-stack";
import { isLiquidGlassAvailable } from "expo-glass-effect";
import { useTheme } from "@/hooks/useTheme";
interface UseScreenOptionsParams {
transparent?: boolean;
}
export function useScreenOptions({
transparent = true,
}: UseScreenOptionsParams = {}): NativeStackNavigationOptions {
const { theme, isDark } = useTheme();
return {
headerTitleAlign: "center",
headerTransparent: transparent,
headerBlurEffect: isDark ? "dark" : "light",
headerTintColor: theme.text,
headerStyle: {
backgroundColor: Platform.select({
ios: undefined,
android: theme.backgroundRoot,
web: theme.backgroundRoot,
}),
},
gestureEnabled: true,
gestureDirection: "horizontal",
fullScreenGestureEnabled: isLiquidGlassAvailable() ? false : true,
contentStyle: {
backgroundColor: theme.backgroundRoot,
},
};
}
Centers header title on all platforms
Makes header transparent on iOS (shows blur effect)
Native blur effect based on color scheme
Enables swipe-back gesture navigation
Sets screen background color to match theme
Navigation Patterns
Type-Safe Navigation
Use TypeScript for compile-time route safety:
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { RootStackParamList } from '@/navigation/RootStackNavigator';
type WorkoutNavigationProp = NativeStackNavigationProp<RootStackParamList, 'Workout'>;
function ProgramScreen() {
const navigation = useNavigation<WorkoutNavigationProp>();
const handleStartWorkout = (week: number, day: string) => {
// TypeScript ensures we pass correct params
navigation.navigate('Workout', { week, day });
};
}
Accessing Route Params
import { RouteProp } from '@react-navigation/native';
import { RootStackParamList } from '@/navigation/RootStackNavigator';
type WorkoutRouteProp = RouteProp<RootStackParamList, 'Workout'>;
function WorkoutScreen() {
const route = useRoute<WorkoutRouteProp>();
const { week, day } = route.params;
return (
<View>
<Text>Week {week} - {day}</Text>
</View>
);
}
Going Back
function WorkoutScreen() {
const navigation = useNavigation();
const handleComplete = async () => {
await saveLoggedWorkout(workout);
navigation.goBack();
};
}
Tab Navigation
import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
import { MainTabParamList } from '@/navigation/MainTabNavigator';
type TabNavigationProp = BottomTabNavigationProp<MainTabParamList>;
function ProgramScreen() {
const navigation = useNavigation<TabNavigationProp>();
const viewHistory = () => {
navigation.navigate('HistoryTab');
};
}
iOS Features
Blur Headers
Translucent headers with native blur effect
Swipe Gestures
Full-screen swipe-back gesture support
Blur Tab Bar
Translucent tab bar using BlurView
Safe Areas
Automatic handling of notches and home indicator
tabBarBackground: () =>
Platform.OS === "ios" ? (
<BlurView
intensity={100}
tint={isDark ? "dark" : "light"}
style={StyleSheet.absoluteFill}
/>
) : null,
Android Features
- Solid backgrounds for better performance
- Back button hardware support (automatic)
- Status bar color coordination
- No blur effects (not supported natively)
tabBarStyle: {
position: "absolute",
backgroundColor: Platform.select({
ios: "transparent",
android: theme.backgroundRoot, // Solid color
}),
borderTopWidth: 0,
elevation: 0,
},
Deep Linking
Deep linking is not yet implemented but can be added via React Navigation’s linking configuration.
Deep Linking Setup (Future)
const linking = {
prefixes: ['rippler://', 'https://rippler.app'],
config: {
screens: {
Main: {
screens: {
ProgramTab: 'program',
HistoryTab: 'history',
GoalsTab: 'goals',
ExercisesTab: 'exercises',
},
},
Workout: 'workout/:week/:day',
},
},
};
<NavigationContainer linking={linking}>
<RootStackNavigator />
</NavigationContainer>
Navigation State Persistence
Navigation state is automatically persisted by React Navigation when the app backgrounds.
To implement custom persistence:
import AsyncStorage from '@react-native-async-storage/async-storage';
const NAVIGATION_STATE_KEY = '@rippler/navigation_state';
function App() {
const [initialState, setInitialState] = useState();
useEffect(() => {
const restoreState = async () => {
const savedState = await AsyncStorage.getItem(NAVIGATION_STATE_KEY);
if (savedState) {
setInitialState(JSON.parse(savedState));
}
};
restoreState();
}, []);
const saveState = (state) => {
AsyncStorage.setItem(NAVIGATION_STATE_KEY, JSON.stringify(state));
};
return (
<NavigationContainer
initialState={initialState}
onStateChange={saveState}
>
<RootStackNavigator />
</NavigationContainer>
);
}
Best Practices
Use TypeScript param lists
Always define param lists for type safety. This prevents runtime errors from incorrect navigation params.
Centralize screen options
Use the useScreenOptions hook to maintain consistent navigation styling across all screens.
Avoid deeply nested navigators
Keep navigator hierarchy shallow (max 3 levels). Deep nesting causes performance issues.
Handle navigation in callbacks
Don’t navigate during render. Use useEffect or event handlers.
Reset navigation state on logout
Use navigation.reset() when user logs out to clear the navigation stack.
Troubleshooting
Screen not updating after navigation
Use useFocusEffect instead of useEffect to run code when screen gains focus:import { useFocusEffect } from '@react-navigation/native';
useFocusEffect(
React.useCallback(() => {
// Runs when screen gains focus
fetchData();
}, [])
);
Tab bar overlapping content
Add bottom padding equal to tab bar height:<ScrollView contentContainerStyle={{ paddingBottom: 80 }}>
{/* Content */}
</ScrollView>
Ensure GestureHandlerRootView wraps the entire app in App.tsx:1
Theming
Learn how navigation adapts to theme changes
Screens
Explore individual screen implementations