Architecture Philosophy
Rippler is built as a modern React Native mobile application using Expo , following a component-based architecture with clear separation of concerns. The app emphasizes:
Type safety with TypeScript
Local-first data with AsyncStorage
Server sync with React Query
Declarative UI with React Native components
Platform-specific optimizations for iOS and Android
Tech Stack
Framework React Native with Expo SDK for cross-platform development
Navigation React Navigation v6 with native stack and tab navigators
State Management React Query for server state, AsyncStorage for local persistence
Type Safety TypeScript for type-safe development
Core Dependencies
{
"@react-navigation/native" : "Navigation container" ,
"@react-navigation/bottom-tabs" : "Tab navigation" ,
"@react-navigation/native-stack" : "Stack navigation" ,
"@tanstack/react-query" : "Server state management" ,
"@react-native-async-storage/async-storage" : "Local storage" ,
"react-native-gesture-handler" : "Gesture system" ,
"react-native-keyboard-controller" : "Keyboard handling" ,
"react-native-safe-area-context" : "Safe area insets" ,
"expo-blur" : "Native blur effects" ,
"expo-status-bar" : "Status bar customization"
}
Application Entry Point
The app bootstraps through a series of context providers that establish the runtime environment:
import React from "react" ;
import { StyleSheet } from "react-native" ;
import { NavigationContainer } from "@react-navigation/native" ;
import { GestureHandlerRootView } from "react-native-gesture-handler" ;
import { KeyboardProvider } from "react-native-keyboard-controller" ;
import { SafeAreaProvider } from "react-native-safe-area-context" ;
import { StatusBar } from "expo-status-bar" ;
import { QueryClientProvider } from "@tanstack/react-query" ;
import { queryClient } from "@/lib/query-client" ;
import RootStackNavigator from "@/navigation/RootStackNavigator" ;
import { ErrorBoundary } from "@/components/ErrorBoundary" ;
export default function App () {
return (
< ErrorBoundary >
< QueryClientProvider client = { queryClient } >
< SafeAreaProvider >
< GestureHandlerRootView style = { styles . root } >
< KeyboardProvider >
< NavigationContainer >
< RootStackNavigator />
</ NavigationContainer >
< StatusBar style = "auto" />
</ KeyboardProvider >
</ GestureHandlerRootView >
</ SafeAreaProvider >
</ QueryClientProvider >
</ ErrorBoundary >
);
}
Provider Hierarchy
ErrorBoundary
Catches React errors and displays fallback UI (client/components/ErrorBoundary.tsx)
QueryClientProvider
Provides React Query client for server state management and API calls
SafeAreaProvider
Handles device-specific safe area insets (notches, status bars)
GestureHandlerRootView
Enables native gesture handling throughout the app
KeyboardProvider
Manages keyboard appearance and dismissal behavior
NavigationContainer
React Navigation root container managing navigation state
Project Structure
client/
├── App.tsx # Application entry point
├── components/ # Reusable UI components
│ ├── ThemedView.tsx # Theme-aware View wrapper
│ ├── ThemedText.tsx # Theme-aware Text wrapper
│ ├── Button.tsx # Custom button component
│ ├── Card.tsx # Card container
│ ├── ErrorBoundary.tsx # Error handling
│ └── ...
├── constants/
│ └── theme.ts # Theme configuration (colors, spacing, typography)
├── data/
│ └── rippler-program.ts # Workout program data
├── hooks/
│ ├── useTheme.ts # Theme hook
│ ├── useColorScheme.ts # Color scheme detection
│ └── useScreenOptions.ts # Navigation screen options
├── lib/
│ ├── storage.ts # AsyncStorage abstraction layer
│ └── query-client.ts # React Query configuration
├── navigation/
│ ├── RootStackNavigator.tsx # Root stack (Main, Workout)
│ ├── MainTabNavigator.tsx # Bottom tabs (Program, Goals, History, Exercises)
│ ├── ProgramStackNavigator.tsx # Program stack
│ ├── HistoryStackNavigator.tsx # History stack
│ ├── ExercisesStackNavigator.tsx # Exercises stack
│ └── GoalsStackNavigator.tsx # Goals stack
├── screens/
│ ├── ProgramScreen.tsx # Main program view
│ ├── WorkoutScreen.tsx # Workout logging
│ ├── HistoryScreen.tsx # Workout history
│ ├── ExercisesScreen.tsx # Exercise management
│ └── GoalsScreen.tsx # Goal tracking
└── types/
└── workout.ts # TypeScript type definitions
Data Layer Architecture
Local Storage
Server State
Type System
AsyncStorage provides persistent local data storage for:
Exercises list (@rippler/exercises)
Logged workouts (@rippler/logged_workouts)
Current week (@rippler/current_week)
Target overrides (@rippler/target_overrides)
Goal weights (@rippler/goal_weights)
See Data Flow for detailed storage patterns. React Query manages server communication with:
Automatic query caching
Background refetching
Optimistic updates
Request deduplication
client/lib/query-client.ts
export const queryClient = new QueryClient ({
defaultOptions: {
queries: {
queryFn: getQueryFn ({ on401: "throw" }),
refetchInterval: false ,
refetchOnWindowFocus: false ,
staleTime: Infinity ,
retry: false ,
},
},
});
TypeScript provides compile-time type safety: export type Tier = "T1" | "T2" | "T3a" | "T3b" ;
export interface Exercise {
id : string ;
name : string ;
notes ?: string ;
}
export interface LoggedWorkout {
id : string ;
week : number ;
day : string ;
dateLogged : string ;
exercises : LoggedExercise [];
completed : boolean ;
}
Design Patterns
Component Composition
Rippler uses composition over inheritance with themed wrapper components:
import { View , type ViewProps } from "react-native" ;
import { useTheme } from "@/hooks/useTheme" ;
export function ThemedView ({ style , lightColor , darkColor , ... otherProps } : ThemedViewProps ) {
const { theme , isDark } = useTheme ();
const backgroundColor = isDark && darkColor
? darkColor
: ! isDark && lightColor
? lightColor
: theme . backgroundRoot ;
return < View style = { [{ backgroundColor }, style ] } { ... otherProps } /> ;
}
Custom Hooks
Business logic is extracted into reusable hooks:
useTheme() - Theme and color scheme access
useColorScheme() - System color scheme detection
useScreenOptions() - Navigation screen configuration
Error Boundaries
Error boundaries catch rendering errors and prevent app crashes:
< ErrorBoundary >
< QueryClientProvider client = { queryClient } >
{ /* App content */ }
</ QueryClientProvider >
</ ErrorBoundary >
Rippler leverages platform-specific features for optimal UX on iOS and Android.
iOS-Specific Features
Blur effects on tab bar and navigation headers using expo-blur
SF Pro Rounded font family for native feel
Translucent headers with native blur
client/navigation/MainTabNavigator.tsx:39
tabBarBackground : () =>
Platform . OS === "ios" ? (
< BlurView
intensity = { 100 }
tint = { isDark ? "dark" : "light" }
style = { StyleSheet . absoluteFill }
/>
) : null ,
Android-Specific Features
Solid backgrounds for better performance
Elevation for depth perception
Roboto font family
Lazy Loading : Components are loaded on-demand
Memoization : React.memo and useMemo prevent unnecessary re-renders
FlatList : Virtualized lists for workout history
Image Optimization : Cached and optimized assets
Bundle Splitting : Code splitting with dynamic imports
Next Steps
Data Flow Learn how data flows through the application
Navigation Understand the navigation structure
Theming Explore the theming system
Components Browse available UI components