The Journey Continues
In Phase 2, I built the authentication flow and navigation structure. Now it's time to make the app feel alive—building the Home screen with personalized greetings, goals management, progress tracking, and a simple health chart.
Coming from React web development, I was curious: How do you build complex layouts in React Native? How do lists and cards work? Can you build charts without libraries? Let me walk you through what I learned, comparing everything to what you already know from React.
What We're Building
In Phase 3, I implemented:
- Home screen layout with greeting header and avatar initials
- "Book Appointment" button (placeholder for Phase 4)
- Goals list with Active/Completed tabs, progress bars, and cards
- Goal details screen with full metadata and latest achievements
- Simple health chart (no library) to visualize strength/flexibility over time
Everything is powered by mock data for now, but the UX structure is ready for backend integration in Phase 8.
Step 1: Project Structure - Adding New Folders
React (Web) - Typical Dashboard Structure:
my-react-app/
├── src/
│ ├── components/
│ │ ├── Dashboard.js
│ │ ├── GoalCard.js
│ │ └── Header.js
│ ├── pages/
│ │ └── Home.js
│ └── data/
│ └── mockGoals.js
React Native (What I Built):
physio-care/
├── src/
│ ├── components/
│ │ ├── screens/
│ │ │ ├── HomeTabMain.tsx # Main home screen
│ │ │ └── GoalDetailsScreen.tsx # Goal detail view
│ │ └── ui-molecules/ # Reusable UI components
│ │ ├── HomeTabHeader.tsx # Greeting + avatar
│ │ ├── GoalCard.tsx # Goal card component
│ │ └── HealthChart.tsx # Simple chart component
│ ├── data/
│ │ └── mockGoals.ts # Mock goal data
│ ├── types/
│ │ └── goal.ts # Goal TypeScript types
│ └── navigation/
│ └── HomeStackNavigator.tsx # Nested navigator for Home
Key Differences:
-
ui-molecules/folder: Small, reusable UI components (like design system atoms/molecules) -
data/folder: Mock data separate from components -
Nested navigation:
HomeStackNavigatorallows pushing detail screens without leaving the tab -
Type definitions: Separate
goal.tsfor TypeScript types
Takeaway: Mobile apps benefit from more granular component organization, especially when building reusable UI elements.
Step 2: Nested Navigation - Stack Inside Tab
This was a new concept for me! In React web, you might use nested routes. In React Native, you can nest navigators.
React (Web) - Nested Routes:
// App.js
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Home from "./pages/Home";
import GoalDetails from "./pages/GoalDetails";
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/goals/:id" element={<GoalDetails />} />
</Routes>
</BrowserRouter>
);
}
React Native - Nested Navigator:
// src/navigation/MainTabNavigator.tsx
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import HomeStackNavigator from "./HomeStackNavigator";
const Tab = createBottomTabNavigator();
export default function MainTabNavigator() {
return (
<Tab.Navigator>
<Tab.Screen
name="Home"
component={HomeStackNavigator} // Stack, not a screen!
/>
{/* Other tabs... */}
</Tab.Navigator>
);
}
// src/navigation/HomeStackNavigator.tsx
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import HomeTabMain from "../components/screens/HomeTabMain";
import GoalDetailsScreen from "../components/screens/GoalDetailsScreen";
import BookAppointmentScreen from "../components/screens/BookAppointmentScreen";
const Stack = createNativeStackNavigator();
export default function HomeStackNavigator() {
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="HomeMain" component={HomeTabMain} />
<Stack.Screen name="GoalDetails" component={GoalDetailsScreen} />
<Stack.Screen name="BookAppointment" component={BookAppointmentScreen} />
</Stack.Navigator>
);
}
Why This Matters:
-
Tab stays active: When you navigate to
GoalDetails, the Home tab remains highlighted - Native back button: Works automatically for stack navigation
- Better UX: Feels more like a native app experience
Navigation Types:
// src/types/navigation.ts
export type HomeStackParamList = {
HomeMain: undefined;
GoalDetails: { goalId: string }; // Requires goalId param
BookAppointment: undefined;
};
export type HomeStackNavigationProp =
import("@react-navigation/native-stack").NativeStackNavigationProp<HomeStackParamList>;
Takeaway: Nested navigators let you build complex navigation flows while keeping tabs persistent. It's like having nested routes, but more structured.
Step 3: Context API - Using User Data
Good news! Context API works exactly the same. I'm using the same AuthContext from Phase 2.
React (Web) - Using Context:
// Dashboard.js
import { useAuth } from "../context/AuthContext";
function Dashboard() {
const { user } = useAuth();
return (
<div>
<h1>Welcome, {user?.name}!</h1>
</div>
);
}
React Native - Using Context (Identical!):
// src/components/screens/HomeTabMain.tsx
import { useAuth } from "../../context/AuthContext";
export default function HomeTabMain() {
const { user } = useAuth();
const userName = user?.name || "User";
return (
<ScrollView>
<HomeTabHeader userName={userName} />
{/* Rest of content... */}
</ScrollView>
);
}
Takeaway: Context API is identical! If you know it in React, you know it in React Native. Zero learning curve.
Step 4: Building the Greeting Header
React (Web) - Header Component:
// Header.js
function Header({ userName }) {
const getGreeting = () => {
const hour = new Date().getHours();
if (hour < 12) return "Good Morning";
if (hour < 17) return "Good Afternoon";
return "Good Evening";
};
const getInitials = (name) => {
return name
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
.slice(0, 2);
};
return (
<header className="header">
<h1>
{getGreeting()}, {userName.split(" ")[0]}!
</h1>
<div className="avatar">{getInitials(userName)}</div>
</header>
);
}
/* Header.css */
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background-color: #fff;
}
.avatar {
width: 44px;
height: 44px;
border-radius: 22px;
background-color: #007aff;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
}
React Native - Header Component:
// src/components/ui-molecules/HomeTabHeader.tsx
import React from "react";
import { View, Text, StyleSheet } from "react-native";
interface HomeTabHeaderProps {
userName: string;
}
export default function HomeTabHeader({ userName }: HomeTabHeaderProps) {
const getGreeting = () => {
const hour = new Date().getHours();
if (hour < 12) return "Good Morning";
if (hour < 17) return "Good Afternoon";
return "Good Evening";
};
const getInitials = (name: string) => {
return name
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
.slice(0, 2);
};
return (
<View style={styles.container}>
<Text style={styles.greeting}>
{getGreeting()}, {userName.split(" ")[0]}!
</Text>
<View style={styles.avatar}>
<Text style={styles.avatarText}>{getInitials(userName)}</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 20,
paddingTop: 20,
paddingBottom: 16,
backgroundColor: "#fff",
},
greeting: {
fontSize: 24,
fontWeight: "bold",
color: "#333",
},
avatar: {
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: "#007AFF",
justifyContent: "center",
alignItems: "center",
},
avatarText: {
color: "#fff",
fontSize: 16,
fontWeight: "600",
},
});
Key Differences:
| React Web | React Native |
|---|---|
<header> and <h1>
|
<View> and <Text>
|
className="header" |
style={styles.container} |
| CSS file with flexbox | StyleSheet with flexbox (same!) |
border-radius: 22px |
borderRadius: 22 |
display: flex (explicit) |
Flexbox by default |
Takeaway: The logic is identical (time-based greeting, initials extraction). The only difference is using React Native components and StyleSheet instead of HTML/CSS.
Step 5: Lists and Filtering - Active/Completed Tabs
React (Web) - Filtered List:
// GoalsList.js
import { useState } from "react";
function GoalsList({ goals }) {
const [activeTab, setActiveTab] = useState("active");
const filteredGoals = goals.filter((goal) => goal.status === activeTab);
return (
<div>
<div className="tabs">
<button
className={activeTab === "active" ? "active" : ""}
onClick={() => setActiveTab("active")}
>
Active
</button>
<button
className={activeTab === "completed" ? "active" : ""}
onClick={() => setActiveTab("completed")}
>
Completed
</button>
</div>
<div className="goals-list">
{filteredGoals.map((goal) => (
<GoalCard key={goal.id} goal={goal} />
))}
</div>
</div>
);
}
/* GoalsList.css */
.tabs {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.tabs button {
flex: 1;
padding: 10px 16px;
border-radius: 8px;
border: 1px solid #e5e5ea;
background-color: #fff;
cursor: pointer;
}
.tabs button.active {
background-color: #007aff;
color: white;
border-color: #007aff;
}
React Native - Filtered List:
// src/components/screens/HomeTabMain.tsx
import { useState } from "react";
import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
import { Goal, GoalTabType } from "../../types/goal";
import GoalCard from "../ui-molecules/GoalCard";
export default function HomeTabMain() {
const [activeTab, setActiveTab] = useState<GoalTabType>("active");
const filteredGoals = mockGoals.filter((goal) => goal.status === activeTab);
return (
<ScrollView>
{/* Tabs */}
<View style={styles.tabs}>
<TouchableOpacity
style={[styles.tab, activeTab === "active" && styles.activeTab]}
onPress={() => setActiveTab("active")}
>
<Text
style={[
styles.tabText,
activeTab === "active" && styles.activeTabText,
]}
>
Active
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.tab, activeTab === "completed" && styles.activeTab]}
onPress={() => setActiveTab("completed")}
>
<Text
style={[
styles.tabText,
activeTab === "completed" && styles.activeTabText,
]}
>
Completed
</Text>
</TouchableOpacity>
</View>
{/* Goals List */}
<View style={styles.goalsList}>
{filteredGoals.length > 0 ? (
filteredGoals.map((goal) => (
<GoalCard key={goal.id} goal={goal} onPress={handleGoalPress} />
))
) : (
<View style={styles.emptyState}>
<Text>No {activeTab} goals yet</Text>
</View>
)}
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
tabs: {
flexDirection: "row",
paddingHorizontal: 20,
marginBottom: 16,
gap: 12,
},
tab: {
flex: 1,
paddingVertical: 10,
paddingHorizontal: 16,
borderRadius: 8,
backgroundColor: "#fff",
alignItems: "center",
borderWidth: 1,
borderColor: "#E5E5EA",
},
activeTab: {
backgroundColor: "#007AFF",
borderColor: "#007AFF",
},
tabText: {
fontSize: 14,
fontWeight: "600",
color: "#666",
textTransform: "capitalize",
},
activeTabText: {
color: "#fff",
},
goalsList: {
paddingHorizontal: 20,
},
emptyState: {
padding: 40,
alignItems: "center",
},
});
Key Differences:
| React Web | React Native |
|---|---|
<button> |
<TouchableOpacity> |
onClick |
onPress |
className with conditional |
style array with conditional |
gap: 12px (CSS) |
gap: 12 (StyleSheet) |
cursor: pointer |
Not needed (touch by default) |
Takeaway: List filtering and conditional styling work the same way. The main difference is using TouchableOpacity instead of button and StyleSheet instead of CSS.
Step 6: Building Reusable Cards - GoalCard Component
React (Web) - Card Component:
// GoalCard.js
function GoalCard({ goal, onClick }) {
return (
<div className="card" onClick={onClick}>
<div className="card-header">
<h3>{goal.name}</h3>
<span>{goal.duration}</span>
</div>
<div className="card-meta">
<span className="type">{goal.type}</span>
<span className="progress">{goal.progress}%</span>
</div>
<div className="progress-bar-container">
<div className="progress-bar-background">
<div
className="progress-bar-fill"
style={{ width: `${goal.progress}%` }}
/>
</div>
</div>
</div>
);
}
/* GoalCard.css */
.card {
background-color: #fff;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
cursor: pointer;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.progress-bar-container {
margin-top: 4px;
}
.progress-bar-background {
height: 8px;
background-color: #e5e5ea;
border-radius: 4px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background-color: #007aff;
border-radius: 4px;
transition: width 0.3s ease;
}
React Native - Card Component:
// src/components/ui-molecules/GoalCard.tsx
import React from "react";
import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
import { Goal } from "../../types/goal";
interface GoalCardProps {
goal: Goal;
onPress: () => void;
}
export default function GoalCard({ goal, onPress }: GoalCardProps) {
return (
<TouchableOpacity style={styles.card} onPress={onPress}>
<View style={styles.header}>
<Text style={styles.name}>{goal.name}</Text>
<Text style={styles.duration}>{goal.duration}</Text>
</View>
<View style={styles.meta}>
<Text style={styles.type}>{goal.type}</Text>
<Text style={styles.progress}>{goal.progress}%</Text>
</View>
{/* Progress bar */}
<View style={styles.progressBarContainer}>
<View style={styles.progressBarBackground}>
<View
style={[styles.progressBarFill, { width: `${goal.progress}%` }]}
/>
</View>
</View>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
card: {
backgroundColor: "#fff",
borderRadius: 12,
padding: 16,
marginBottom: 12,
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3, // Android shadow
},
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 8,
},
name: {
fontSize: 16,
fontWeight: "600",
color: "#333",
flex: 1,
},
duration: {
fontSize: 14,
color: "#666",
},
meta: {
flexDirection: "row",
justifyContent: "space-between",
marginBottom: 12,
},
type: {
fontSize: 14,
color: "#007AFF",
},
progress: {
fontSize: 14,
fontWeight: "600",
color: "#333",
},
progressBarContainer: {
marginTop: 4,
},
progressBarBackground: {
height: 8,
backgroundColor: "#E5E5EA",
borderRadius: 4,
overflow: "hidden",
},
progressBarFill: {
height: "100%",
backgroundColor: "#007AFF",
borderRadius: 4,
},
});
Key Differences:
| React Web | React Native |
|---|---|
box-shadow |
shadowColor, shadowOffset, shadowOpacity + elevation
|
cursor: pointer |
TouchableOpacity (handles touch) |
onClick |
onPress |
transition: width 0.3s |
No transitions (can use Animated API) |
div with style inline |
View with style array for dynamic values |
Progress Bar Implementation:
Both use the same technique:
- Outer container (background color)
- Inner element with dynamic width based on percentage
- React Native uses
width: \${goal.progress}%`` in style array
Takeaway: Cards work the same way conceptually. The main differences are shadow implementation (more verbose in React Native) and using TouchableOpacity for touch interactions.
Step 7: Navigation with Parameters - Goal Details Screen
React (Web) - Route Parameters:
``
// App.js
import { BrowserRouter, Routes, Route } from "react-router-dom";
import GoalDetails from "./pages/GoalDetails";
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/goals/:id" element={<GoalDetails />} />
</Routes>
</BrowserRouter>
);
}
// GoalDetails.js
import { useParams, useNavigate } from "react-router-dom";
function GoalDetails() {
const { id } = useParams();
const navigate = useNavigate();
const goal = goals.find((g) => g.id === id);
return (
<div>
<button onClick={() => navigate(-1)}>← Back</button>
<h1>{goal.name}</h1>
{/* Rest of details... */}
</div>
);
}
React Native - Navigation Parameters:
// Navigate from GoalCard
navigation.navigate("GoalDetails", { goalId: goal.id });
// src/components/screens/GoalDetailsScreen.tsx
import { useNavigation, useRoute, RouteProp } from "@react-navigation/native";
import { HomeStackParamList } from "../../types/navigation";
type GoalDetailsRouteProp = RouteProp<HomeStackParamList, "GoalDetails">;
export default function GoalDetailsScreen() {
const navigation = useNavigation();
const route = useRoute<GoalDetailsRouteProp>();
const { goalId } = route.params; // Get goalId from params
const goal = mockGoals.find((g) => g.id === goalId);
if (!goal) {
return (
<View>
<Text>Goal not found</Text>
</View>
);
}
return (
<View style={styles.container}>
{/* Header with back button */}
<View style={styles.header}>
<TouchableOpacity
onPress={() => navigation.goBack()}
style={styles.backButtonContainer}
activeOpacity={0.7}
>
<Text style={styles.backButton}>← Back</Text>
</TouchableOpacity>
</View>
<ScrollView contentContainerStyle={styles.content}>
{/* Goal details... */}
</ScrollView>
</View>
);
}
Key Differences:
| React Web | React Native |
|---|---|
useParams() hook |
route.params from useRoute()
|
useNavigate() hook |
navigation prop (or useNavigation()) |
navigate(-1) |
navigation.goBack() |
URL-based (/goals/:id) |
Screen name-based ('GoalDetails') |
| TypeScript types optional | TypeScript types required for safety |
Takeaway: Navigation parameters work similarly, but React Navigation is more type-safe. You define params in TypeScript types, and the compiler ensures you pass the correct data.
Step 8: ScrollView vs Regular View - Handling Long Content
React (Web) - Scrolling:
// Home.js
function Home() {
return (
<div className="home-container">
<Header />
<GoalsList />
<Chart />
</div>
);
}
/* Home.css */
.home-container {
height: 100vh;
overflow-y: auto; /* Scroll if content exceeds viewport */
}
React Native - ScrollView:
// src/components/screens/HomeTabMain.tsx
import { ScrollView } from "react-native";
export default function HomeTabMain() {
return (
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
<HomeTabHeader userName={userName} />
<TouchableOpacity style={styles.bookButton}>
<Text>+ Book an appointment</Text>
</TouchableOpacity>
<View style={styles.goalsSection}>{/* Goals list... */}</View>
<HealthChart />
</ScrollView>
);
}
Key Differences:
| React Web | React Native |
|---|---|
overflow-y: auto (CSS) |
<ScrollView> component |
| Automatic scrolling | Explicit ScrollView component |
height: 100vh |
flex: 1 (takes available space) |
| Scrollbar always visible |
showsVerticalScrollIndicator={false} to hide |
Important Note: In React Native, you must use ScrollView for scrollable content. Regular View components don't scroll automatically.
Takeaway: ScrollView is like a div with overflow: auto, but it's a component you explicitly use. It's more intentional and gives you more control.
Step 9: Building a Simple Chart Without Libraries
React (Web) - Chart with CSS:
// Chart.js
function Chart({ data }) {
const maxValue = Math.max(...data.map((d) => d.value));
return (
<div className="chart">
{data.map((point, index) => (
<div key={index} className="bar-group">
<div
className="bar"
style={{ height: `${(point.value / maxValue) * 100}%` }}
/>
<span>{point.value}</span>
</div>
))}
</div>
);
}
/* Chart.css */
.chart {
display: flex;
justify-content: space-around;
align-items: flex-end;
height: 200px;
}
.bar-group {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.bar {
width: 100%;
background-color: #007aff;
border-radius: 4px;
min-height: 4px;
}
React Native - Chart with View Components:
// src/components/ui-molecules/HealthChart.tsx
import React from "react";
import { View, Text, StyleSheet } from "react-native";
interface ChartDataPoint {
date: string;
strength: number;
flexibility: number;
}
export default function HealthChart({
title = "Knee Extension: Seated (90)",
data = mockChartData,
}: HealthChartProps) {
const maxValue = Math.max(
...data.map((d) => Math.max(d.strength, d.flexibility))
);
return (
<View style={styles.container}>
<Text style={styles.title}>{title}</Text>
<View style={styles.chartContainer}>
{data.map((point, index) => (
<View key={index} style={styles.barGroup}>
<View style={styles.bars}>
{/* Strength bar */}
<View style={styles.barContainer}>
<View
style={[
styles.bar,
styles.strengthBar,
{ height: `${(point.strength / maxValue) * 100}%` },
]}
/>
<Text style={styles.barLabel}>{point.strength}</Text>
</View>
{/* Flexibility bar */}
<View style={styles.barContainer}>
<View
style={[
styles.bar,
styles.flexibilityBar,
{ height: `${(point.flexibility / maxValue) * 100}%` },
]}
/>
<Text style={styles.barLabel}>{point.flexibility}</Text>
</View>
</View>
<Text style={styles.dateLabel}>{point.date}</Text>
</View>
))}
</View>
{/* Legend */}
<View style={styles.legend}>
<View style={styles.legendItem}>
<View style={[styles.legendColor, styles.strengthBar]} />
<Text style={styles.legendText}>Strength</Text>
</View>
<View style={styles.legendItem}>
<View style={[styles.legendColor, styles.flexibilityBar]} />
<Text style={styles.legendText}>Flexibility</Text>
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: "#fff",
borderRadius: 12,
padding: 16,
marginHorizontal: 20,
marginBottom: 20,
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
chartContainer: {
flexDirection: "row",
justifyContent: "space-around",
alignItems: "flex-end",
height: 200,
marginBottom: 16,
},
barGroup: {
alignItems: "center",
flex: 1,
},
bars: {
flexDirection: "row",
alignItems: "flex-end",
height: "100%",
width: "100%",
justifyContent: "center",
},
barContainer: {
alignItems: "center",
marginHorizontal: 4,
flex: 1,
height: "100%",
justifyContent: "flex-end",
},
bar: {
width: "100%",
minHeight: 4,
borderRadius: 4,
marginBottom: 4,
},
strengthBar: {
backgroundColor: "#007AFF",
},
flexibilityBar: {
backgroundColor: "#34C759",
},
barLabel: {
fontSize: 10,
color: "#666",
marginTop: 4,
},
dateLabel: {
fontSize: 10,
color: "#666",
marginTop: 8,
},
legend: {
flexDirection: "row",
justifyContent: "center",
gap: 20,
},
legendItem: {
flexDirection: "row",
alignItems: "center",
},
legendColor: {
width: 12,
height: 12,
borderRadius: 2,
marginRight: 6,
},
legendText: {
fontSize: 12,
color: "#666",
},
});
Key Differences:
| React Web | React Native |
|---|---|
div with flexbox |
View with flexbox |
CSS height percentage |
Style array with percentage string |
gap (modern CSS) |
gap (React Native supports it!) |
| Multiple bars per group | Same approach, just more Views |
Chart Logic (Same in Both):
- Calculate
maxValuefrom all data points - Map data to bar groups
- Set bar height as percentage:
(value / maxValue) * 100% - Use flexbox for layout
Takeaway: You can build simple charts without libraries in both React and React Native. The logic is identical; only the components differ (div vs View).
Step 10: TypeScript Types for Goals
This was important for type safety! Let me show you the goal types I created.
React (Web) - JavaScript Objects:
// mockGoals.js
export const mockGoals = [
{
id: "1",
name: "Improve Knee Strength",
duration: "6 weeks",
type: "Strength",
progress: 50,
priority: "high",
status: "active",
targetDate: "2024-12-15",
current: 25,
target: 50,
unit: "kg",
},
];
React Native - TypeScript Types:
// src/types/goal.ts
export interface Goal {
id: string;
name: string;
duration: string; // e.g., "6 weeks"
type: string; // e.g., "Strength", "Flexibility"
progress: number; // 0-100 percentage
priority: "high" | "medium" | "low";
status: "active" | "completed";
targetDate?: string;
lastUpdated?: string;
current?: number;
target?: number;
unit?: string; // e.g., "kg", "degrees"
latestAchievement?: string;
}
export type GoalTabType = "active" | "completed";
// src/data/mockGoals.ts
import { Goal } from "../types/goal";
export const mockGoals: Goal[] = [
{
id: "1",
name: "Improve Knee Strength",
duration: "6 weeks",
type: "Strength",
progress: 50,
priority: "high",
status: "active",
targetDate: "2024-12-15",
lastUpdated: "2024-11-01",
current: 25,
target: 50,
unit: "kg",
latestAchievement: "Increased weight to 25kg this week",
},
{
id: "2",
name: "Increase Flexibility",
duration: "4 weeks",
type: "Flexibility",
progress: 75,
priority: "medium",
status: "active",
targetDate: "2024-11-30",
lastUpdated: "2024-11-05",
current: 45,
target: 60,
unit: "degrees",
latestAchievement: "Reached 45 degrees flexion",
},
{
id: "3",
name: "Reduce Pain",
duration: "8 weeks",
type: "Pain Management",
progress: 100,
priority: "high",
status: "completed",
targetDate: "2024-10-20",
lastUpdated: "2024-10-18",
current: 0,
target: 0,
unit: "pain scale",
latestAchievement: "Pain reduced to zero",
},
];
Benefits of TypeScript:
- Autocomplete: IDE suggests available properties
-
Type Safety: Compiler catches typos (e.g.,
goal.progres→ error!) - Documentation: Types serve as inline documentation
- Refactoring: Easier to rename properties across codebase
Takeaway: TypeScript types are optional in React web, but they're highly recommended in React Native (especially for navigation). They prevent bugs and improve developer experience.
Step 11: SafeAreaView - Handling Status Bar
This was a new concept! In React web, you don't worry about status bars. In React Native, you need to account for the device's status bar and notches.
React (Web) - No Status Bar:
// Header.js
function Header() {
return (
<header style={{ paddingTop: 20 }}>
<h1>Welcome</h1>
</header>
);
}
React Native - SafeAreaView:
// src/components/screens/GoalDetailsScreen.tsx
import { SafeAreaView } from "react-native-safe-area-context";
export default function GoalDetailsScreen() {
return (
<View style={styles.container}>
<SafeAreaView edges={["top"]} style={styles.safeArea}>
<View style={styles.header}>
<TouchableOpacity onPress={() => navigation.goBack()}>
<Text>← Back</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
{/* Rest of content... */}
</View>
);
}
Why SafeAreaView?
- Notches: iPhone X+ have notches that content shouldn't overlap
- Status Bar: Android/iOS status bars take up space
-
Automatic:
SafeAreaViewautomatically adds padding to avoid these areas
Alternative (Manual Padding):
// If you don't use SafeAreaView
header: {
paddingTop: 50, // Manual padding (not ideal)
}
Takeaway: SafeAreaView ensures your content doesn't overlap with system UI. It's mobile-specific and doesn't have a web equivalent.
Problems I Faced & How I Solved Them
Problem 1: Back Button Not Clickable
Issue: The back button in GoalDetailsScreen wasn't responding to touches.
Root Cause: The back button was inside a ScrollView, which can interfere with touch events.
Solution: Moved the header outside the ScrollView:
// ❌ Before (not clickable)
<ScrollView>
<View style={styles.header}>
<TouchableOpacity onPress={() => navigation.goBack()}>
<Text>← Back</Text>
</TouchableOpacity>
</View>
{/* Content... */}
</ScrollView>
// ✅ After (clickable)
<View style={styles.container}>
<View style={styles.header}>
<TouchableOpacity onPress={() => navigation.goBack()}>
<Text>← Back</Text>
</TouchableOpacity>
</View>
<ScrollView>
{/* Content... */}
</ScrollView>
</View>
Also Added:
-
minHeight: 44for proper touch target size -
activeOpacity={0.7}for visual feedback - Proper padding for larger hit area
Takeaway: Keep interactive elements (buttons) outside ScrollView when possible, or ensure they have proper touch target sizes.
Problem 2: ScrollView Style Conflict
Issue: Used style={styles.container} on both outer View and ScrollView, causing layout issues.
Solution: Created separate styles:
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#f5f5f5",
},
scrollView: {
flex: 1, // For ScrollView
},
content: {
padding: 20, // For ScrollView content
},
});
Takeaway: Use style for the ScrollView container and contentContainerStyle for the scrollable content padding.
Problem 3: Tab State Not Persisting
Issue: When navigating to GoalDetails and back, the tab selection (Active/Completed) reset.
Current Behavior: This is expected with local state. The tab resets when the component unmounts/remounts.
Future Solution (Phase 8): When connecting to backend, we can:
- Store tab preference in context or async storage
- Use navigation state to preserve tab selection
- Or accept that it resets (simpler UX)
Takeaway: Local state resets on navigation. If you need persistence, use context or storage.
Problem 4: Chart Bars Not Scaling Correctly
Issue: Chart bars were too small or too large, not proportional to data.
Solution: Calculate maxValue correctly and use percentage-based heights:
// ✅ Correct
const maxValue = Math.max(
...data.map((d) => Math.max(d.strength, d.flexibility))
);
<View
style={[styles.bar, { height: `${(point.strength / maxValue) * 100}%` }]}
/>;
Takeaway: Always calculate relative values (percentages) based on the maximum value in your dataset.
What I Tested
Here's my testing checklist for Phase 3:
✅ Authentication Flow (Prerequisites)
- [x] Login → OTP → Profile Setup → Home navigation works
- [x] User data persists in context after login
✅ Home Screen - Greeting Header
- [x] Greeting changes based on time of day (Morning/Afternoon/Evening)
- [x] User name displays correctly from context
- [x] Avatar shows correct initials (first letter of each word)
- [x] Header layout looks good on different screen sizes
✅ Book Appointment Button
- [x] Button is visible and clickable
- [x] Navigates to
BookAppointmentScreen - [x] Back button works correctly
- [x] Placeholder text displays
✅ Goals List - Tabs
- [x] "Active" tab selected by default
- [x] Tabs filter goals correctly (Active shows 2, Completed shows 1)
- [x] Tab highlighting works (blue when active)
- [x] Switching tabs updates list immediately
✅ Goal Cards
- [x] Cards display all information (name, duration, type, progress)
- [x] Progress bars show correct percentage
- [x] Cards are clickable and navigate to details
- [x] Cards have proper shadows/elevation
- [x] Empty state shows when no goals in selected tab
✅ Goal Details Screen
- [x] Navigation from card works
- [x] All goal data displays correctly
- [x] Progress bar matches card progress
- [x] Current/Target values show with units
- [x] Metadata (Priority, Status, Dates) displays
- [x] Latest Achievement section shows
- [x] Back button is clickable and works
- [x] Header doesn't scroll (fixed position)
✅ Health Chart
- [x] Chart displays with title
- [x] Bars scale proportionally to data
- [x] Two bar types (Strength/Flexibility) visible
- [x] Values display above bars
- [x] Date labels show below bars
- [x] Legend displays correctly
- [x] Chart scrolls with page content
✅ Overall Navigation
- [x] Bottom tabs work (Home, Timeline, Support, Profile)
- [x] Home tab stays active when navigating to details
- [x] Back navigation works from all screens
- [x] No navigation errors or warnings
What I Learned: Key Takeaways
Nested Navigation is Powerful: Stack navigators inside tabs let you build complex flows while keeping tabs persistent.
ScrollView is Explicit: Unlike web where scrolling is automatic, React Native requires explicit
ScrollViewcomponents.Touch Targets Matter: Buttons need proper sizing (
minHeight: 44) and shouldn't be insideScrollViewwhen possible.TypeScript Types Prevent Bugs: Navigation types and goal types caught several errors during development.
Simple Charts are Possible: You can build basic visualizations with
Viewcomponents and flexbox—no library needed for simple cases.SafeAreaView for System UI: Always account for status bars and notches using
SafeAreaView.Mock Data First: Building with mock data lets you focus on UX before worrying about API integration.
Common Questions (If You're Coming from React)
Q: Can I use CSS-in-JS libraries like styled-components?
A: Yes! styled-components has React Native support. Many developers use it instead of StyleSheet.
Q: How do I handle long lists efficiently?
A: Use FlatList instead of ScrollView with map(). FlatList only renders visible items (virtualization). I'll cover this in a future phase.
Q: Can I use chart libraries?
A: Yes! Libraries like react-native-chart-kit or victory-native work well. I built a simple one to learn, but you can use libraries for production.
Q: How do I handle images?
A: Use <Image> component with source={{ uri: '...' }} or require('./image.png'). I'll cover this in future phases.
Q: Can I use the same state management libraries?
A: Yes! Redux, Zustand, Jotai—they all work with React Native. Context API (which I'm using) is built-in and works great for simple cases.
Q: How do I debug layout issues?
A: Use React Native Debugger or add colored borders to View components: borderWidth: 1, borderColor: 'red' to see component boundaries.
Resources That Helped Me
- React Navigation - Nested Navigators - Great guide on nesting
- React Native - ScrollView - Official ScrollView docs
- React Native - SafeAreaView - Handling system UI
- TypeScript Handbook - Type definitions
Code Repository
All the code from Phase 3 is available on GitHub:
- physio-care-react-native-first-project - Complete source code
Top comments (0)