Mobile UI: Mobile App Workflow
This guide explains how the mobile app works from a workflow perspective – how components, navigation, authentication, and data fetching all work together to create a functional mobile application.
Quick Overview
When the mobile app starts, here’s what happens:
- App Initializes → Providers set up (auth, theme, data fetching, error handling)
- Auth Check → App checks for stored authentication token
- Route Decision → If authenticated → tabs; if not → login screen
- User Interacts → Navigation between screens, fetching data as needed
- Data Updates → React Query automatically caches and manages server data
- State Changes → Components re-render with new data
The app uses:
- Expo Router for file-based navigation
- React Query for server data fetching and caching
- React Context for global authentication state
- Service Layer for organized API calls
1. App Initialization
When the app starts, it goes through a specific initialization sequence:
1.1. Root Layout (app/_layout.tsx)
The root layout sets up the app’s provider hierarchy:
<SafeAreaProvider>
<ErrorBoundary>
<PaperProvider theme={appTheme}>
<QueryClientProvider client={queryClient}>
<AuthProvider>
<Stack>
{/* Routes */}
</Stack>
</AuthProvider>
</QueryClientProvider>
</PaperProvider>
</ErrorBoundary>
</SafeAreaProvider>
Provider Order (from outside in):
- SafeAreaProvider - Handles safe areas (notches, status bars)
- ErrorBoundary - Catches and displays errors gracefully
- PaperProvider - Provides React Native Paper theme
- QueryClientProvider - Provides React Query for data fetching
- AuthProvider - Manages authentication state
- Stack - Expo Router navigation
What each provider does:
AuthProvider→ Authentication state (user, login, logout)QueryClientProvider→ React Query for data fetchingPaperProvider→ React Native Paper themeSafeAreaProvider→ Safe area insets for notches/status bars
Note: See Reference: Deep Dives for detailed explanations of providers and provider order.
1.2. Entry Point (app/index.tsx)
The entry point checks authentication and routes accordingly:
export default function Index() {
const { user, loading } = useAuth();
const router = useRouter();
useEffect(() => {
if (loading) return; // Wait for auth check
if (user) {
router.replace('/(tabs)/home'); // Authenticated → tabs
} else {
router.replace('/(auth)/home'); // Not authenticated → auth
}
}, [user, loading]);
}
Flow:
- App loads →
index.tsxrenders AuthProviderchecks for stored token. If the token exists → fetch user data- If authenticated → navigate to
/(tabs)/home - If not authenticated → navigate to
/(auth)/home
Note: AuthProvider is only available because the Stack is wrapped in <AuthProvider> (defined in _layout.tsx). If AuthProvider is removed or placed outside the <Stack>, this will throw an error.
2. Authentication Flow
2.1. Login Process
- User enters credentials in
app/(auth)/login.tsx - Calls
login()fromAuthContext:const { login } = useAuth(); await login(username, password); - AuthContext makes API call:
const response = await apiClient.post( '/api/auth/login', { username, password } ); - Token stored securely:
// SecureStore (native) or localStorage (web) await setItem('auth_token', token); - User data fetched and stored:
const userResponse = await apiClient.get('/api/auth/me'); setUser(userResponse.data); - Navigation updates - Router automatically navigates to tabs
2.2. Protected Routes
Screens that require authentication (e.g., tabs/courses.tsx) use the ProtectedRoute component:
<ProtectedRoute>
<YourScreen />
</ProtectedRoute>
How it works:
- Checks if user is authenticated
- If not → redirects to login
- If yes → renders the screen
2.3. Token Management
- Storage: Tokens stored in
SecureStore(native) orlocalStorage(web) - Automatic inclusion: API client automatically adds token to request headers
- Refresh:
AuthContextchecks token validity on app start - Logout: Removes token and clears user state
3. Navigation
The app uses Expo Router for file-based routing.
3.1. Route Groups
(auth)/- Authentication screens (login, home)(tabs)/- Main app screens with tab navigation
3.2. Tab Navigation
The (tabs)/_layout.tsx defines the bottom tab bar:
<Tabs>
<Tabs.Screen name="home" />
<Tabs.Screen name="groups" />
<Tabs.Screen name="courses" />
<Tabs.Screen name="progress" />
<Tabs.Screen name="profile" />
{/* Detail screens - hidden from tab bar */}
<Tabs.Screen name="courses/[id]" options={{ href: null }} />
</Tabs>
Key Points:
- Main tabs appear in bottom navigation
- Detail screens (like
courses/[id]) are hidden from tab bar but still accessible - Dynamic routes use
[id]syntax
3.3. Navigation Patterns
Navigate to a tab:
router.push('/(tabs)/courses');
Navigate to a detail screen:
router.push(`/(tabs)/courses/${courseId}`);
Navigate back:
router.back();
Replace current screen:
router.replace('/(tabs)/home');
3.4. Screen Workflow Example
Let’s trace through viewing a course:
1. User Taps Course Card
// In courses.tsx
<TouchableOpacity
onPress={() => router.push(`/(tabs)/courses/${course.id}`)}
>
<CourseCard course={course} />
</TouchableOpacity>
2. Course Detail Screen Loads
// In courses/[id].tsx
export default function CourseDetailScreen() {
const { id } = useLocalSearchParams();
const { data: course } = useQuery({
queryKey: ['course', id],
queryFn: () => getCourseDetail(Number(id)),
});
// Render course details
}
3. Data Fetching Flow
- React Query checks cache for
['course', id] - If not cached → calls
getCourseDetail(id) - Service function makes API call:
apiClient.get('/api/courses/${id}') - API client adds auth token to headers
- Response cached and component re-renders
4. User Navigates to Module
<TouchableOpacity
onPress={() => router.push(`/(tabs)/modules/${module.module_id}`)}
>
<ModuleCard module={module} />
</TouchableOpacity>
4. Data Fetching
The app uses React Query (TanStack Query) for all server data fetching.
4.1. What is React Query?
React Query is a data-fetching library that simplifies fetching, caching, and updating server state. Instead of manually managing loading states, error handling, and caching with useState and useEffect, React Query handles this automatically.
Key Benefits:
- Automatic Caching - Data is cached and reused across components
- Loading & Error States - Automatically tracked for you
- Background Refetching - Keeps data fresh automatically
- Deduplication - Multiple requests for the same data = one network call
Note: See Reference: Deep Dives for a detailed explanation of React Query features.
4.2. Basic Pattern
import { useQuery } from '@tanstack/react-query';
import { getUserCourses } from '../../services/courses';
export default function CoursesScreen() {
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['userCourses'],
queryFn: getUserCourses,
});
if (isLoading) return <ActivityIndicator />;
if (error) return <Text>Error loading courses</Text>;
return (
// Render courses
);
}
4.3. Query Keys
Query keys identify cached data:
['userCourses']- List of user’s courses['course', courseId]- Specific course details['groups']- List of groups
4.4. Service Layer
API calls are organized in the services/ directory:
services/
├── api.ts # Base API client
├── courses.ts # Course-related API calls
├── groups.ts # Group-related API calls
├── posts.ts # Post-related API calls
├── progress.ts # Progress-related API calls
└── users.ts # User-related API calls
Example service function:
// services/courses.ts
export async function getUserCourses(): Promise<Course[]> {
const response = await apiClient.get<Course[]>('/api/courses/');
return response.data;
}
Used in components:
const { data: courses } = useQuery({
queryKey: ['userCourses'],
queryFn: getUserCourses, // Service function
});
4.5. API Client
The services/api.ts file provides a centralized API client:
Features:
- Automatic token inclusion - Adds auth token to headers
- Platform-aware URL - Handles emulator vs. physical device networking
- Error handling - Standardized error responses
- Request/response interceptors - Logging and error transformation
Usage:
import apiClient from '../services/api';
// GET request
const response = await apiClient.get<User>('/api/auth/me');
// POST request
const response = await apiClient.post<Token>('/api/auth/login', {
username,
password,
});
5. State Management
The app uses a hybrid approach:
5.1. React Context (Auth State)
- What: Authentication state (user, token)
- Where:
contexts/AuthContext.tsx - Why: Global state needed across all screens
- Usage:
const { user, login, logout } = useAuth();
5.2. React Query (Server State)
- What: Data from API (courses, groups, posts)
- Where: Cached by React Query
- Why: Automatic caching, refetching, synchronization
- Usage:
const { data } = useQuery({ ... });
5.3. Local State (Component State)
- What: UI state (form inputs, modals, loading)
- Where:
useStatein components - Why: Component-specific, doesn’t need sharing
- Usage:
const [isOpen, setIsOpen] = useState(false);
6. Error Handling
6.1. API Errors
The API client handles errors consistently:
try {
const response = await apiClient.get('/api/courses');
} catch (error) {
if (error instanceof ApiError) {
// Handle API error (401, 404, 500, etc.)
console.error('API Error:', error.status, error.message);
}
}
6.2. React Query Errors
React Query provides error states:
const { data, error, isLoading } = useQuery({ ... });
if (error) {
return <Text>Error: {error.message}</Text>;
}
6.3. Global Error Boundary
The ErrorBoundary component catches unhandled errors:
<ErrorBoundary>
<App />
</ErrorBoundary>
7. Common Patterns
7.1. Pull-to-Refresh
Pull-to-refresh allows users to manually refresh data by dragging down on a scrollable list. This is a common mobile pattern that gives users control over when to fetch fresh data from the server.
const { data, refetch, isRefetching } = useQuery({ ... });
<ScrollView
refreshControl={
<RefreshControl refreshing={isRefetching} onRefresh={refetch} />
}
>
{/* Content */}
</ScrollView>
7.2. Loading States
Loading states show users that data is being fetched, preventing confusion when the screen appears empty or unresponsive. React Query’s isLoading flag makes it easy to display a spinner or skeleton screen while data loads.
const { data, isLoading } = useQuery({ ... });
if (isLoading) {
return <ActivityIndicator />;
}
7.3. Empty States
Empty states are displayed when a list or data collection has no items to show. Instead of showing a blank screen, you can provide helpful messaging that explains why there’s no data and what the user can do next.
if (!data || data.length === 0) {
return <Text>No courses found</Text>;
}
8. Reference: Deep Dives
8.1. What is a Provider?
A provider is a React component that uses React Context to make data or functionality available to all its child components. Instead of passing data through props at every level, providers allow you to:
- Wrap your app with a provider component
- Share data/functions with any child component that needs it
- Access the data using hooks like
useContext()or custom hooks (e.g.,useAuth())
Example:
// Provider wraps the app
<AuthProvider>
<App />
</AuthProvider>
// Any child component can access auth data
function MyComponent() {
const { user, login } = useAuth(); // Gets data from AuthProvider
// ...
}
8.2. Does Provider Order Matter?
Yes, but with some flexibility:
Critical Rules:
- ErrorBoundary must be high in the tree to catch errors from providers below it
- Stack must be inside all providers so screens can access all contexts
- If Provider A uses hooks from Provider B, Provider B must wrap Provider A
Flexible Order:
SafeAreaProvider,PaperProvider,QueryClientProvider, andAuthProvidercan be reordered as long as they all wrapStack- These providers don’t depend on each other, so their relative order doesn’t matter
In This App:
- All providers wrap
Stack ErrorBoundaryis high enough to catch errors- No provider depends on another provider’s context
- The current order is a good convention but not strictly required
8.3. React Query: Detailed Features
React Query provides:
-
Automatic Caching: Data fetched from the API is automatically cached. If you request the same data again (using the same query key), React Query returns the cached data instantly while optionally refetching in the background to ensure freshness.
-
Loading & Error States: React Query automatically tracks whether data is loading, has loaded successfully, or encountered an error, eliminating the need to manually manage these states.
-
Background Refetching: React Query can automatically refetch data in the background when the app regains focus, when the network reconnects, or at specified intervals, keeping your data fresh without user interaction.
-
Deduplication: If multiple components request the same data simultaneously, React Query makes only one network request and shares the result, preventing unnecessary API calls.
-
Optimistic Updates: You can update the UI immediately before the server responds, then roll back if the request fails, providing a snappier user experience.
Think of React Query as a “smart data layer” that sits between your React components and your API, handling all the complex state management, caching, and synchronization logic so you can focus on building your UI.
Summary
App Flow:
- App initializes → Providers set up
- Auth check → Token validation
- Route decision → Authenticated or login screen
- User interacts → Navigation and data fetching
- Data updates → React Query manages cache
- State changes → Components re-render
Key Technologies:
- Expo Router - File-based navigation
- React Query - Server state management
- React Context - Global auth state
- React Native Paper - UI components
- SecureStore - Secure token storage
Best Practices:
- Use React Query for all server data
- Use Context only for truly global state (auth)
- Use local state for component-specific UI
- Organize API calls in service layer
- Protect routes that require authentication
- Handle loading and error states