DEV Community

Cover image for Phase 3: Building the Home Experience - Goals, Details, and a Simple Chart
Md Enayetur Rahman
Md Enayetur Rahman

Posted on

Phase 3: Building the Home Experience - Goals, Details, and a Simple Chart

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:

  1. Home screen layout with greeting header and avatar initials
  2. "Book Appointment" button (placeholder for Phase 4)
  3. Goals list with Active/Completed tabs, progress bars, and cards
  4. Goal details screen with full metadata and latest achievements
  5. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Key Differences:

  1. ui-molecules/ folder: Small, reusable UI components (like design system atoms/molecules)
  2. data/ folder: Mock data separate from components
  3. Nested navigation: HomeStackNavigator allows pushing detail screens without leaving the tab
  4. Type definitions: Separate goal.ts for 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode
// 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>;
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode
/* 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;
}
Enter fullscreen mode Exit fullscreen mode

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",
  },
});
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode
/* 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;
}
Enter fullscreen mode Exit fullscreen mode

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",
  },
});
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode
/* 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;
}
Enter fullscreen mode Exit fullscreen mode

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,
  },
});
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode
// 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode
/* Home.css */
.home-container {
  height: 100vh;
  overflow-y: auto; /* Scroll if content exceeds viewport */
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode
/* 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;
}
Enter fullscreen mode Exit fullscreen mode

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",
  },
});
Enter fullscreen mode Exit fullscreen mode

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):

  1. Calculate maxValue from all data points
  2. Map data to bar groups
  3. Set bar height as percentage: (value / maxValue) * 100%
  4. 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",
  },
];
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode
// 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",
  },
];
Enter fullscreen mode Exit fullscreen mode

Benefits of TypeScript:

  1. Autocomplete: IDE suggests available properties
  2. Type Safety: Compiler catches typos (e.g., goal.progres → error!)
  3. Documentation: Types serve as inline documentation
  4. 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Why SafeAreaView?

  • Notches: iPhone X+ have notches that content shouldn't overlap
  • Status Bar: Android/iOS status bars take up space
  • Automatic: SafeAreaView automatically adds padding to avoid these areas

Alternative (Manual Padding):

// If you don't use SafeAreaView
header: {
  paddingTop: 50, // Manual padding (not ideal)
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Also Added:

  • minHeight: 44 for 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
  },
});
Enter fullscreen mode Exit fullscreen mode

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}%` }]}
/>;
Enter fullscreen mode Exit fullscreen mode

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

  1. Nested Navigation is Powerful: Stack navigators inside tabs let you build complex flows while keeping tabs persistent.

  2. ScrollView is Explicit: Unlike web where scrolling is automatic, React Native requires explicit ScrollView components.

  3. Touch Targets Matter: Buttons need proper sizing (minHeight: 44) and shouldn't be inside ScrollView when possible.

  4. TypeScript Types Prevent Bugs: Navigation types and goal types caught several errors during development.

  5. Simple Charts are Possible: You can build basic visualizations with View components and flexbox—no library needed for simple cases.

  6. SafeAreaView for System UI: Always account for status bars and notches using SafeAreaView.

  7. 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


Code Repository

All the code from Phase 3 is available on GitHub:

Top comments (0)