The Journey Continues
In Phase 7, I completed the patient-facing experience with the Profile area, clinical records, payments, and regimen tracking. The app was working perfectly—every feature functional, every screen looking good. But then I took a step back and looked at my code with fresh eyes. And I saw it: code duplication everywhere.
Avatar components with different logic in three places. Progress bars copy-pasted across four files. Back buttons implemented seven different ways. Tab components, status badges, search inputs—all duplicated with slight variations. My web developer instincts kicked in: "This violates DRY (Don't Repeat Yourself)!"
Coming from React web development, I knew the power of reusable components. But mobile development added new challenges: How do you create truly reusable components when each screen has slightly different requirements? How do you balance consistency with flexibility? What's the right level of abstraction?
This phase is about going back and refactoring—turning duplicated code into clean, reusable components that follow best practices. Let me walk you through what I found and how I fixed it.
What We're Building
In Phase 7.5, I refactored the entire codebase to create:
- Atomic UI components - Avatar, Button, ProgressBar, BackButton, SearchInput, StatusBadge
- Molecular UI components - ScreenHeader, TabGroup, EmptyState
- Shared utilities - Status color functions, common styles
- Consistent patterns - Standardized component APIs across the app
- Type-safe props - TypeScript interfaces for every component
- Migration strategy - How to refactor without breaking existing features
The goal: Eliminate duplication while making the codebase more maintainable and consistent.
Step 1: The Code Audit - Finding Duplication
Before refactoring, I needed to understand what was duplicated. I analyzed every screen component and found patterns.
What I Found:
// Avatar logic - 3 different implementations
// ProfileScreen.tsx - Single letter
<View style={styles.avatar}>
<Text>{user?.name?.charAt(0)?.toUpperCase() || 'U'}</Text>
</View>
// ChatScreen.tsx - Two letters from each word
<Text>{item.name.split(' ').map(n => n[0]).join('').slice(0, 2)}</Text>
// HomeTabHeader.tsx - Function with logic
const getInitials = (name: string) => {
return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
};
Problem: Same functionality, three different implementations. If I need to change avatar logic, I'd have to update three places.
More Issues Found:
- Progress bars duplicated in 4 files with different heights/colors
-
Back buttons implemented 7 different ways (
←,‹, "← Back", with/without containers) - Tabs repeated in 3 screens with similar but not identical code
- Status badges with color logic copy-pasted 5+ times
- Search inputs duplicated in 3 places
- Empty states repeated in 4 screens
- Button styles duplicated across 5+ screens
Takeaway: Even a well-structured app accumulates duplication as features are added. Regular code audits reveal patterns that should be extracted into reusable components.
Step 2: Creating the Avatar Component
Let's start with the Avatar—a perfect example of how slight variations lead to duplication.
Before - Three Different Implementations:
// ProfileScreen.tsx
<View style={styles.avatar}>
<Text style={styles.avatarText}>
{user?.name?.charAt(0)?.toUpperCase() || 'U'}
</Text>
</View>
// Styles: 60px, single letter
// ChatScreen.tsx
<View style={styles.avatarContainer}>
<Text style={styles.avatarText}>
{item.name.split(' ').map(n => n[0]).join('').slice(0, 2)}
</Text>
</View>
// Styles: 50px, two letters
// HomeTabHeader.tsx
const getInitials = (name: string) => {
return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
};
<View style={styles.avatar}>
<Text style={styles.avatarText}>{getInitials(userName)}</Text>
</View>
// Styles: 44px, two letters with function
After - One Reusable Component:
// src/components/ui-atoms/Avatar.tsx
interface AvatarProps {
name: string;
size?: number;
backgroundColor?: string;
textColor?: string;
maxInitials?: number; // 1 or 2
style?: ViewStyle;
}
export default function Avatar({
name,
size = 44,
backgroundColor = "#007AFF",
textColor = "#fff",
maxInitials = 2,
style,
}: AvatarProps) {
const getInitials = (name: string, max: number) => {
const parts = name.trim().split(" ").filter(Boolean);
if (parts.length === 0) return "U";
if (max === 1) {
return parts[0][0].toUpperCase();
}
return parts
.slice(0, max)
.map((part) => part[0])
.join("")
.toUpperCase()
.slice(0, max);
};
return (
<View
style={[
styles.container,
{
width: size,
height: size,
borderRadius: size / 2,
backgroundColor,
},
style,
]}
>
<Text
style={[
styles.text,
{
fontSize: size * 0.36,
color: textColor,
},
]}
>
{getInitials(name, maxInitials)}
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
justifyContent: "center",
alignItems: "center",
},
text: {
fontWeight: "bold",
},
});
Usage - Clean and Consistent:
// ProfileScreen.tsx - Single letter, 60px
<Avatar name={user?.name || 'User'} maxInitials={1} size={60} />
// ChatScreen.tsx - Two letters, 50px
<Avatar name={item.name} size={50} />
// HomeTabHeader.tsx - Two letters, default size (44px)
<Avatar name={userName} />
Key Design Decisions:
-
Flexible sizing:
sizeprop controls all dimensions dynamically -
Font scaling: Font size calculated as
size * 0.36for consistent proportions -
Configurable initials:
maxInitialsallows 1 or 2 letters - Smart defaults: Common case (2 letters, 44px, blue) requires no props
-
Style override:
styleprop allows positioning adjustments - Fallback handling: Returns 'U' if name is empty
Benefits:
- 73 lines of code eliminated across three files
- One source of truth for avatar logic
- Consistent behavior across all screens
- Easy to customize per use case
- Type-safe with TypeScript
Takeaway: Good reusable components balance flexibility with sensible defaults. The 80% case should be a one-liner; the 20% case should be possible with props.
Step 3: Creating the ProgressBar Component
Progress bars were duplicated in four files with similar structure but different styling.
Before - Four Different Implementations:
// GoalCard.tsx
<View style={styles.progressBarContainer}>
<View style={styles.progressBarBackground}>
<View style={[styles.progressBarFill, { width: `${goal.progress}%` }]} />
</View>
</View>
// Styles: 8px height, blue fill
// SessionCard.tsx
<View style={styles.progressBar}>
<View style={[styles.progressFill, { width: `${progressPercentage}%` }]} />
</View>
// Styles: 4px height, green fill
// RegimenTab.tsx
<View style={styles.progressBar}>
<View style={[styles.progressFill, { width: `${(completed/total)*100}%` }]} />
</View>
// Styles: 6px height, blue fill
// GoalDetailsScreen.tsx
<View style={styles.progressBarContainer}>
<View style={styles.progressBarBackground}>
<View style={[styles.progressBarFill, { width: `${goal.progress}%` }]} />
</View>
</View>
// Styles: 12px height, blue fill
Problem: Same pattern, slight variations in height and color. Lots of duplicated StyleSheet definitions.
After - One Reusable Component:
// src/components/ui-atoms/ProgressBar.tsx
interface ProgressBarProps {
progress: number; // 0-100
height?: number;
backgroundColor?: string;
fillColor?: string;
showLabel?: boolean;
label?: string;
borderRadius?: number;
}
export default function ProgressBar({
progress,
height = 8,
backgroundColor = "#E5E5EA",
fillColor = "#007AFF",
showLabel = false,
label,
borderRadius = 4,
}: ProgressBarProps) {
const clampedProgress = Math.min(100, Math.max(0, progress));
return (
<View>
{showLabel && label && <Text style={styles.label}>{label}</Text>}
<View
style={[
styles.container,
{
height,
backgroundColor,
borderRadius,
},
]}
>
<View
style={[
styles.fill,
{
width: `${clampedProgress}%`,
backgroundColor: fillColor,
borderRadius,
},
]}
/>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
overflow: "hidden",
},
fill: {
height: "100%",
},
label: {
fontSize: 14,
color: "#666",
marginBottom: 4,
},
});
Usage - Simple and Flexible:
// GoalCard.tsx - Default 8px height, blue
<ProgressBar progress={goal.progress} />
// SessionCard.tsx - 4px height, green
<ProgressBar progress={progressPercentage} height={4} fillColor="#34C759" />
// RegimenTab.tsx - 6px height, blue
<ProgressBar progress={(completed / total) * 100} height={6} />
// GoalDetailsScreen.tsx - 12px height, with label
<ProgressBar
progress={goal.progress}
height={12}
showLabel
label={`${goal.progress}% completed`}
/>
Key Design Decisions:
- Progress clamping: Ensures value stays between 0-100
- Dynamic styling: All visual properties configurable via props
- Optional label: Can show text above progress bar
- Overflow hidden: Prevents fill from exceeding container
- Percentage-based width: Works with React Native's layout system
Benefits:
- 120+ lines of code eliminated across four files
- Consistent progress bar behavior everywhere
- Visual variants easy to create with props
- No style duplication in screen components
Takeaway: Progress bars are a perfect candidate for extraction—they're visually consistent but need slight variations. A well-designed component handles all cases with props.
Step 4: Creating the Button Component
Buttons were the worst offender—duplicated across 5+ screens with almost identical styles.
Before - Duplicated Button Styles:
// LoginScreen.tsx
<TouchableOpacity style={styles.button} onPress={handleSendOTP}>
<Text style={styles.buttonText}>Send OTP</Text>
</TouchableOpacity>;
const styles = StyleSheet.create({
button: {
backgroundColor: "#007AFF",
borderRadius: 8,
padding: 16,
alignItems: "center",
},
buttonText: {
color: "#fff",
fontSize: 16,
fontWeight: "600",
},
});
// OTPScreen.tsx - Exact same styles repeated
// UserDetailsScreen.tsx - Exact same styles repeated
// ProfileScreen.tsx - Red button for logout
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>
<Text style={styles.logoutText}>Logout</Text>
</TouchableOpacity>;
const styles = StyleSheet.create({
logoutButton: {
backgroundColor: "#FF3B30", // Only difference: color
borderRadius: 12,
padding: 16,
alignItems: "center",
},
logoutText: {
color: "#fff",
fontSize: 16,
fontWeight: "600",
},
});
// BookAppointmentScreen.tsx - With disabled state
<TouchableOpacity
style={[
styles.proceedButton,
(!selectedCenter || !selectedConsultant) && styles.disabledButton,
]}
onPress={handleProceedToBooking}
disabled={!selectedCenter || !selectedConsultant}
>
<Text
style={[
styles.proceedButtonText,
(!selectedCenter || !selectedConsultant) && styles.disabledButtonText,
]}
>
Proceed
</Text>
</TouchableOpacity>;
Problem: Same button logic repeated everywhere. Disabled state logic duplicated. Variant styles (primary, danger) not standardized.
After - One Reusable Component:
// src/components/ui-atoms/Button.tsx
interface ButtonProps {
title: string;
onPress: () => void;
variant?: "primary" | "secondary" | "danger";
disabled?: boolean;
fullWidth?: boolean;
style?: ViewStyle;
textStyle?: TextStyle;
}
export default function Button({
title,
onPress,
variant = "primary",
disabled = false,
fullWidth = true,
style,
textStyle,
}: ButtonProps) {
return (
<TouchableOpacity
onPress={onPress}
activeOpacity={0.7}
disabled={disabled}
style={[
styles.base,
styles[variant],
fullWidth && styles.fullWidth,
disabled && styles.disabled,
style,
]}
>
<Text
style={[
styles.text,
styles[`${variant}Text`],
disabled && styles.disabledText,
textStyle,
]}
>
{title}
</Text>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
base: {
borderRadius: 12,
padding: 16,
alignItems: "center",
justifyContent: "center",
},
fullWidth: {
width: "100%",
},
primary: {
backgroundColor: "#007AFF",
},
primaryText: {
color: "#fff",
fontSize: 16,
fontWeight: "600",
},
danger: {
backgroundColor: "#FF3B30",
},
dangerText: {
color: "#fff",
fontSize: 16,
fontWeight: "600",
},
secondary: {
backgroundColor: "#fff",
borderWidth: 1,
borderColor: "#007AFF",
},
secondaryText: {
color: "#007AFF",
fontSize: 16,
fontWeight: "600",
},
disabled: {
backgroundColor: "#E5E5EA",
},
disabledText: {
color: "#999",
},
});
Usage - Clean and Declarative:
// LoginScreen.tsx - Primary button
<Button title="Send OTP" onPress={handleSendOTP} />
// OTPScreen.tsx - Primary button
<Button title="Verify OTP" onPress={handleVerify} />
// UserDetailsScreen.tsx - Primary button
<Button title="Proceed to Home" onPress={handleProceed} />
// ProfileScreen.tsx - Danger variant
<Button title="Logout" onPress={handleLogout} variant="danger" />
// BookAppointmentScreen.tsx - With disabled state
<Button
title={sessionType === 'online' ? 'Proceed to Video Consultation' : 'Book Appointment'}
onPress={handleProceedToBooking}
disabled={!selectedCenter || !selectedConsultant}
/>
// VideoConsultationScreen.tsx - With disabled state
<Button
title="Confirm Booking"
onPress={handleBookConsultation}
disabled={!selectedDate || !selectedTimeSlot}
/>
Key Design Decisions:
- Variant system: Three button types (primary, secondary, danger) with automatic styling
- Disabled state: Automatically handles visual and functional disabled state
- Full width default: Most mobile buttons are full-width; easy to override
- Style composition: Base styles + variant styles + disabled styles combine cleanly
- Accessible: Properly disabled for screen readers
Benefits:
- 200+ lines of code eliminated across 5+ screens
- Consistent button behavior everywhere
- Easy to add variants (just add to StyleSheet)
- Disabled state handled automatically
- Type-safe props prevent invalid combinations
Takeaway: Buttons are fundamental UI elements. Standardizing them early creates consistency across the entire app. Variant systems are powerful for managing multiple button types.
Step 5: Creating the ScreenHeader Component
Headers were duplicated in 10+ screens with slight variations.
Before - Duplicated Header Code:
// ClinicalRecordsScreen.tsx
<View style={styles.header}>
<TouchableOpacity style={styles.backButton} onPress={() => navigation.goBack()}>
<Text style={styles.backArrow}>‹</Text>
</TouchableOpacity>
<Text style={styles.title}>Clinical Records</Text>
</View>
// PaymentScreen.tsx - Almost identical
<View style={styles.header}>
<TouchableOpacity style={styles.backButton} onPress={() => navigation.goBack()}>
<Text style={styles.backArrow}>‹</Text>
</TouchableOpacity>
<Text style={styles.title}>Payments</Text>
</View>
// BookAppointmentScreen.tsx - Different back button style
<View style={styles.header}>
<TouchableOpacity onPress={() => navigation.goBack()}>
<Text style={styles.backButton}>← Back</Text>
</TouchableOpacity>
<Text style={styles.title}>Book Appointment</Text>
</View>
// TimelineTab.tsx - No back button
<View style={styles.header}>
<Text style={styles.headerTitle}>My Appointments</Text>
</View>
// SupportTab.tsx - With subtitle
<View style={styles.header}>
<Text style={styles.greeting}>How can we help you, {userName}?</Text>
<Text style={styles.subtitle}>We're here to support your physiotherapy journey</Text>
</View>
Problem: Same header structure repeated everywhere. Back button logic duplicated. Different padding, different back button styles, inconsistent.
After - One Flexible Component:
// src/components/ui-molecules/ScreenHeader.tsx
import BackButton from "../ui-atoms/BackButton";
interface ScreenHeaderProps {
title?: string;
subtitle?: string;
showBackButton?: boolean;
onBackPress?: () => void;
rightComponent?: React.ReactNode;
paddingTop?: number;
backgroundColor?: string;
}
export default function ScreenHeader({
title,
subtitle,
showBackButton = false,
onBackPress,
rightComponent,
paddingTop = 40,
backgroundColor = "#fff",
}: ScreenHeaderProps) {
return (
<View style={[styles.container, { paddingTop, backgroundColor }]}>
<View style={styles.content}>
{showBackButton && onBackPress && <BackButton onPress={onBackPress} />}
<View style={styles.titleContainer}>
{title && <Text style={styles.title}>{title}</Text>}
{subtitle && <Text style={styles.subtitle}>{subtitle}</Text>}
</View>
{rightComponent && <View style={styles.right}>{rightComponent}</View>}
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
borderBottomWidth: 1,
borderBottomColor: "#E5E5EA",
},
content: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 20,
paddingBottom: 12,
},
titleContainer: {
flex: 1,
},
title: {
fontSize: 20,
fontWeight: "bold",
color: "#333",
},
subtitle: {
fontSize: 16,
color: "#666",
marginTop: 4,
},
right: {
marginLeft: 12,
},
});
Usage - Simple and Consistent:
// ClinicalRecordsScreen.tsx - With back button
<ScreenHeader
title="Clinical Records"
showBackButton
onBackPress={() => navigation.goBack()}
/>
// PaymentScreen.tsx - With back button
<ScreenHeader
title="Payments"
showBackButton
onBackPress={() => navigation.goBack()}
/>
// BookAppointmentScreen.tsx - With back button
<ScreenHeader
title="Book Appointment"
showBackButton
onBackPress={() => navigation.goBack()}
/>
// TimelineTab.tsx - Title only
<ScreenHeader title="My Appointments" />
// SupportTab.tsx - With subtitle
<ScreenHeader
title={`How can we help you, ${userName}?`}
subtitle="We're here to support your physiotherapy journey"
/>
// VideoConsultationScreen.tsx - Custom padding
<ScreenHeader
title="Video Consultation"
showBackButton
onBackPress={() => navigation.goBack()}
paddingTop={60}
/>
Key Design Decisions:
- Optional back button: Show/hide with boolean prop
- Flexible content: Title, subtitle, and right component all optional
- Consistent spacing: Standardized padding and margins
- Integration: Uses BackButton component internally
- Customizable: Padding and background color configurable
Benefits:
- 300+ lines of code eliminated across 10+ screens
- Consistent header spacing everywhere
- Easy to add features (right icons, custom content)
- Composable: Works with BackButton component
Takeaway: Composite components (molecules) that use atomic components create a coherent design system. ScreenHeader + BackButton is more powerful than either alone.
Step 6: Creating the TabGroup Component
Tabs were implemented three times with similar but not identical code.
Before - Three Tab Implementations:
// HomeTabMain.tsx
<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>
// TimelineTab.tsx - With counts
<View style={styles.tabsContainer}>
{tabs.map((tab) => (
<TouchableOpacity
key={tab.key}
style={[styles.tab, activeTab === tab.key && styles.activeTab]}
onPress={() => setActiveTab(tab.key)}
>
<Text style={[styles.tabText, activeTab === tab.key && styles.activeTabText]}>
{tab.label} ({tab.count})
</Text>
</TouchableOpacity>
))}
</View>
// RegimenTab.tsx - Three tabs
<View style={styles.tabs}>
{tabs.map((tab) => (
<TouchableOpacity
key={tab.key}
style={[styles.tab, activeTab === tab.key && styles.activeTab]}
onPress={() => setActiveTab(tab.key as RegimenTabType)}
>
<Text style={[styles.tabText, activeTab === tab.key && styles.activeTabText]}>
{tab.label}
</Text>
</TouchableOpacity>
))}
</View>
Problem: Same tab pattern repeated. Count display inconsistent. Type safety inconsistent.
After - One Generic Component:
// src/components/ui-molecules/TabGroup.tsx
interface Tab {
key: string;
label: string;
count?: number;
}
interface TabGroupProps<T extends string> {
tabs: Tab[];
activeTab: T;
onTabChange: (tab: T) => void;
showCounts?: boolean;
style?: ViewStyle;
}
export default function TabGroup<T extends string>({
tabs,
activeTab,
onTabChange,
showCounts = false,
style,
}: TabGroupProps<T>) {
return (
<View style={[styles.container, style]}>
{tabs.map((tab) => (
<TouchableOpacity
key={tab.key}
style={[styles.tab, activeTab === tab.key && styles.activeTab]}
onPress={() => onTabChange(tab.key as T)}
>
<Text
style={[
styles.tabText,
activeTab === tab.key && styles.activeTabText,
]}
>
{tab.label}
{showCounts && tab.count !== undefined && ` (${tab.count})`}
</Text>
</TouchableOpacity>
))}
</View>
);
}
const styles = StyleSheet.create({
container: {
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",
},
});
Usage - Type-Safe and Flexible:
// HomeTabMain.tsx - Two tabs
<TabGroup
tabs={[
{ key: 'active', label: 'Active' },
{ key: 'completed', label: 'Completed' },
]}
activeTab={activeTab}
onTabChange={setActiveTab}
/>
// TimelineTab.tsx - With counts
<TabGroup
tabs={[
{ key: 'upcoming', label: 'Upcoming', count: 5 },
{ key: 'completed', label: 'Completed', count: 12 },
{ key: 'cancelled', label: 'Cancelled', count: 2 },
]}
activeTab={activeTab}
onTabChange={setActiveTab}
showCounts
/>
// RegimenTab.tsx - Three tabs
<TabGroup
tabs={[
{ key: 'not-started', label: 'Not Started' },
{ key: 'in-progress', label: 'In Progress' },
{ key: 'completed', label: 'Completed' },
]}
activeTab={activeTab}
onTabChange={setActiveTab}
/>
Key Design Decisions:
-
Generic type parameter:
<T extends string>ensures type safety for tab keys - Optional counts: Show/hide count display with boolean prop
- Flexible tab data: Array of tab objects with key, label, and optional count
-
Equal width tabs:
flex: 1distributes space evenly - Consistent styling: All tabs look the same across the app
Benefits:
- 150+ lines of code eliminated across three screens
- Type-safe tab switching prevents invalid keys
- Consistent tab behavior everywhere
- Easy to add tabs (just add to array)
Takeaway: Generic TypeScript components provide type safety while remaining reusable. The <T extends string> pattern is powerful for components that work with custom types.
Step 7: Creating Shared Utilities (statusColors.ts)
Status color logic was duplicated in 5+ places.
Before - Duplicated Color Functions:
// SessionCard.tsx
const getStatusColor = (status: string) => {
switch (status) {
case "upcoming":
return "#007AFF";
case "completed":
return "#34C759";
case "cancelled":
return "#FF3B30";
default:
return "#8E8E93";
}
};
// AppointmentDetailsSheet.tsx - Exact same function
const getStatusColor = (status: string) => {
switch (status) {
case "upcoming":
return "#007AFF";
case "completed":
return "#34C759";
case "cancelled":
return "#FF3B30";
default:
return "#8E8E93";
}
};
// PreviousTicketsScreen.tsx - Two functions
const getStatusColor = (status: string) => {
switch (status) {
case "open":
return "#34C759";
case "closed":
return "#8E8E93";
case "pending":
return "#FF9500";
default:
return "#8E8E93";
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case "high":
return "#FF3B30";
case "medium":
return "#FF9500";
case "low":
return "#34C759";
default:
return "#8E8E93";
}
};
// RegimenTab.tsx - Inline logic
backgroundColor: regimen.status === "completed"
? "#34C759"
: regimen.status === "in-progress"
? "#FF9500"
: "#8E8E93";
Problem: Same color mappings repeated everywhere. No single source of truth. Hard to change colors globally.
After - Shared Utility Functions:
// src/utils/statusColors.ts
export const getStatusColor = (status: string): string => {
const statusMap: Record<string, string> = {
// Appointments
upcoming: "#007AFF",
completed: "#34C759",
cancelled: "#FF3B30",
// Regimens
"in-progress": "#FF9500",
"not-started": "#8E8E93",
// Support tickets
open: "#34C759",
closed: "#8E8E93",
pending: "#FF9500",
};
return statusMap[status.toLowerCase()] || "#8E8E93";
};
export const getPriorityColor = (priority: string): string => {
const priorityMap: Record<string, string> = {
high: "#FF3B30",
medium: "#FF9500",
low: "#34C759",
};
return priorityMap[priority.toLowerCase()] || "#8E8E93";
};
// Optional: Type-safe version
export type Status =
| "upcoming"
| "completed"
| "cancelled"
| "in-progress"
| "not-started"
| "open"
| "closed"
| "pending";
export const STATUS_COLORS: Record<Status, string> = {
upcoming: "#007AFF",
completed: "#34C759",
cancelled: "#FF3B30",
"in-progress": "#FF9500",
"not-started": "#8E8E93",
open: "#34C759",
closed: "#8E8E93",
pending: "#FF9500",
};
Usage - Simple Imports:
// SessionCard.tsx
import { getStatusColor } from '../../utils/statusColors';
<Text style={[styles.status, { color: getStatusColor(session.status) }]}>
{session.status}
</Text>
// AppointmentDetailsSheet.tsx
import { getStatusColor } from '../../utils/statusColors';
<Text style={[styles.infoValue, { color: getStatusColor(session.status) }]}>
{session.status}
</Text>
// PreviousTicketsScreen.tsx
import { getStatusColor, getPriorityColor } from '../../utils/statusColors';
<View style={[styles.statusBadge, { backgroundColor: getStatusColor(item.status) }]}>
<Text style={styles.statusText}>{item.status}</Text>
</View>
<View style={[styles.priorityBadge, { backgroundColor: getPriorityColor(item.priority) }]}>
<Text style={styles.priorityText}>{item.priority}</Text>
</View>
// RegimenTab.tsx
import { getStatusColor } from '../../utils/statusColors';
<View style={[styles.statusBadge, { backgroundColor: getStatusColor(regimen.status) }]}>
<Text style={styles.statusText}>{regimen.status}</Text>
</View>
Key Design Decisions:
- Single source of truth: All color mappings in one file
-
Lowercase normalization:
.toLowerCase()makes it case-insensitive - Default fallback: Unknown statuses get gray color
- Multiple functions: Separate functions for different domains (status vs priority)
- Optional type safety: Can use Record type for strict typing
Benefits:
- 100+ lines of code eliminated across 5+ files
- Easy to change colors globally (change once, update everywhere)
- Consistent color usage across all status badges
- Simple to add new statuses (just add to object)
Takeaway: Don't duplicate business logic—even simple mappings like colors. Utility files create single sources of truth and make global changes trivial.
Step 8: Creating Common Styles (styles/common.ts)
Container styles were repeated in every screen component.
Before - Duplicated Container Styles:
// Every screen had this:
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#f5f5f5",
},
// Or variations:
container: {
flex: 1,
backgroundColor: "#fff",
},
container: {
flex: 1,
backgroundColor: "#f8f9fa",
},
});
// Card styles repeated:
const styles = StyleSheet.create({
card: {
backgroundColor: "#fff",
borderRadius: 12,
padding: 16,
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
});
// Section title styles repeated:
const styles = StyleSheet.create({
sectionTitle: {
fontSize: 18,
fontWeight: "600",
color: "#333",
marginBottom: 12,
},
});
Problem: Same styles repeated in 15+ files. Inconsistent spacing. Hard to maintain design system.
After - Shared Style Constants:
// src/styles/common.ts
import { StyleSheet } from "react-native";
export const commonStyles = StyleSheet.create({
// Containers
container: {
flex: 1,
backgroundColor: "#f5f5f5",
},
containerWhite: {
flex: 1,
backgroundColor: "#fff",
},
scrollContainer: {
flexGrow: 1,
paddingBottom: 20,
},
// Layout
screenPadding: {
paddingHorizontal: 20,
},
section: {
marginBottom: 20,
},
// Cards
card: {
backgroundColor: "#fff",
borderRadius: 12,
padding: 16,
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
// Typography
sectionTitle: {
fontSize: 18,
fontWeight: "600",
color: "#333",
marginBottom: 12,
},
heading: {
fontSize: 24,
fontWeight: "bold",
color: "#333",
},
body: {
fontSize: 16,
color: "#666",
lineHeight: 24,
},
caption: {
fontSize: 14,
color: "#999",
},
});
// Optional: Design tokens
export const colors = {
primary: "#007AFF",
secondary: "#5856D6",
success: "#34C759",
warning: "#FF9500",
danger: "#FF3B30",
gray: "#8E8E93",
lightGray: "#E5E5EA",
background: "#f5f5f5",
backgroundWhite: "#fff",
text: "#333",
textSecondary: "#666",
textLight: "#999",
};
export const spacing = {
xs: 4,
sm: 8,
md: 12,
lg: 16,
xl: 20,
xxl: 24,
};
export const borderRadius = {
sm: 8,
md: 12,
lg: 16,
full: 9999,
};
Usage - Import and Use:
// Any screen component
import { commonStyles, colors, spacing } from "../../styles/common";
export default function ProfileScreen() {
return (
<ScrollView
style={commonStyles.container}
showsVerticalScrollIndicator={false}
>
<View style={[commonStyles.card, { marginTop: spacing.xxl }]}>
<Text style={commonStyles.heading}>Patient Info</Text>
<Text style={commonStyles.body}>Details here...</Text>
</View>
</ScrollView>
);
}
// Custom styles can extend common styles
const styles = StyleSheet.create({
customCard: {
...commonStyles.card,
borderWidth: 1,
borderColor: colors.primary,
},
customText: {
...commonStyles.body,
fontStyle: "italic",
},
});
Key Design Decisions:
- Common patterns: Extract frequently used style combinations
- Design tokens: Separate colors, spacing, border radius for reuse
- Extensible: Spread syntax allows customization
- Platform-aware: Elevation (Android) and shadow (iOS) both included
- Typography scale: Consistent text styles across app
Benefits:
- 500+ lines of code eliminated across all screens
- Consistent spacing and colors everywhere
- Easy to change design system (update tokens, all screens update)
- Reduced StyleSheet bloat in individual components
Takeaway: Common styles and design tokens are the foundation of a design system. Start with colors and spacing, then add common patterns (cards, typography) as you find them.
Key Learnings
1. DRY is About Logic, Not Lines of Code
Don't extract duplication just to reduce lines. Extract when:
- Logic is identical (same algorithm, different data)
- Changes affect multiple places (update once vs many times)
- Patterns are established (3+ occurrences means pattern)
Bad extraction:
// Over-abstraction - not worth extracting
const Spacer = ({ size }) => <View style={{ height: size }} />;
Good extraction:
// Clear benefit - reusable with variations
<ProgressBar progress={75} height={8} fillColor="#34C759" />
2. Component API Design Matters
Good reusable components have:
- Sensible defaults - Common case requires minimal props
- Clear customization - Props for expected variations
- Escape hatches - Style prop for edge cases
- Type safety - TypeScript prevents misuse
- Composability - Works well with other components
Example:
// Good: Sensible defaults, clear customization
<Avatar name="John Doe" /> // Default: 44px, 2 letters, blue
<Avatar name="Jane" maxInitials={1} /> // Custom: 1 letter
<Avatar name="Bob" size={60} /> // Custom: larger size
3. Build a Component Hierarchy
Organize components by complexity:
- Atoms - Basic building blocks (Button, Avatar, ProgressBar)
- Molecules - Combinations of atoms (ScreenHeader, TabGroup)
- Organisms - Complex components (forms, lists)
- Templates - Screen layouts
Example:
ui-atoms/
├── Avatar.tsx (simple, no dependencies)
├── Button.tsx (simple, no dependencies)
└── BackButton.tsx (simple, no dependencies)
ui-molecules/
├── ScreenHeader.tsx (uses BackButton)
├── TabGroup.tsx (uses Button concepts)
└── EmptyState.tsx (simple molecule)
4. Utilities for Business Logic
Extract shared logic to utilities:
- Color mappings (status colors, priority colors)
- Date formatting (consistent formats across screens)
- Validation (phone numbers, emails, etc.)
- Calculations (progress percentages, totals)
Example:
// utils/statusColors.ts - Shared color logic
// utils/dateFormatters.ts - Shared date formatting
// utils/validators.ts - Shared validation logic
5. Migration Strategy Matters
Don't refactor everything at once:
- Create components in parallel with existing code
- Update one screen at a time
- Test thoroughly after each screen
- Remove old code only after all screens migrated
- Document patterns for team consistency
Gradual migration:
// Week 1: Create Avatar component
// Week 2: Update ProfileScreen to use Avatar
// Week 3: Update ChatScreen to use Avatar
// Week 4: Update HomeTabHeader to use Avatar
// Week 5: Remove old avatar code
6. TypeScript Makes Refactoring Safer
TypeScript catches breaking changes during refactoring:
// Change component API
interface ButtonProps {
title: string; // Was: text: string
onPress: () => void;
variant?: string;
}
// TypeScript errors show everywhere that needs updating
<Button text="Click" /> // ❌ Error: Property 'text' does not exist
<Button title="Click" /> // ✅ Correct
Takeaway: Comprehensive TypeScript types make large refactors less scary. Compiler finds every place that needs updating.
Common Patterns & Refactoring Strategies
1. The "Three Strike Rule"
When to extract a component:
- First time: Write it inline
- Second time: Copy-paste is okay (notice the duplication)
- Third time: Extract to reusable component
Why: You need examples to identify the pattern. Premature abstraction is worse than duplication.
2. The "Props Explosion" Warning
Bad component API (too many props):
interface BadButtonProps {
title: string;
onPress: () => void;
backgroundColor?: string;
textColor?: string;
borderColor?: string;
borderWidth?: number;
borderRadius?: number;
paddingVertical?: number;
paddingHorizontal?: number;
fontSize?: number;
fontWeight?: string;
// ... 20 more props
}
Better: Use variants + style escape hatch:
interface GoodButtonProps {
title: string;
onPress: () => void;
variant?: "primary" | "secondary" | "danger";
disabled?: boolean;
style?: ViewStyle; // Escape hatch for custom styles
textStyle?: TextStyle; // Escape hatch for text styles
}
Takeaway: If your component has 10+ props, it's trying to do too much. Use variants for common cases and style props for edge cases.
3. The "Composition Over Configuration" Pattern
Bad: One mega-component with props for everything:
<Header
showBackButton
showSearchButton
showMenuButton
showAvatar
title="Screen"
subtitle="Description"
onBackPress={...}
onSearchPress={...}
onMenuPress={...}
avatarName="John"
/>
Good: Composable components:
<ScreenHeader
title="Screen"
subtitle="Description"
leftComponent={<BackButton onPress={...} />}
rightComponent={<Avatar name="John" />}
/>
Takeaway: Let components be composed together rather than configuring one component with many props.
4. The "Default Props" Pattern
Good defaults reduce API surface:
export default function ProgressBar({
progress,
height = 8, // Most progress bars are 8px
backgroundColor = "#E5E5EA", // Standard background
fillColor = "#007AFF", // Primary blue
borderRadius = 4, // Matches height
}: ProgressBarProps) {
// ...
}
// Usage - minimal props needed
<ProgressBar progress={75} />; // Uses all defaults
Takeaway: Design for the 80% case. Most uses should need minimal props. The 20% case can provide custom props.
5. The "Extract Utils Early" Pattern
Don't wait to extract utility functions:
// ❌ Bad: Color logic inline everywhere
<View
style={{ backgroundColor: status === "completed" ? "#34C759" : "#FF3B30" }}
/>;
// ✅ Good: Extract to utility immediately
import { getStatusColor } from "../../utils/statusColors";
<View style={{ backgroundColor: getStatusColor(status) }} />;
Takeaway: Utility functions are easy to extract and have high ROI. Do it early before duplication spreads.
Migration Checklist
Use this checklist when refactoring your codebase:
Phase 1: Audit
- [ ] List all duplicated patterns in codebase
- [ ] Count occurrences of each pattern
- [ ] Identify which patterns have highest duplication
- [ ] Document variations in each pattern
Phase 2: Design
- [ ] Design component API (props, defaults)
- [ ] Create TypeScript interfaces
- [ ] Write example usage code
- [ ] Get feedback on API design
Phase 3: Implement
- [ ] Create component in ui-atoms or ui-molecules
- [ ] Add comprehensive prop types
- [ ] Implement with sensible defaults
- [ ] Test component in isolation
Phase 4: Migrate
- [ ] Update one screen at a time
- [ ] Test each screen after update
- [ ] Fix any issues before moving to next screen
- [ ] Remove old code after full migration
Phase 5: Document
- [ ] Add JSDoc comments to component
- [ ] Create usage examples
- [ ] Document props and their purposes
- [ ] Update component library documentation
Before vs After: The Numbers
Here's what the refactoring achieved:
| Pattern | Files Affected | Lines Removed | Lines Added | Net Change |
|---|---|---|---|---|
| Avatar | 3 | 73 | 45 | -28 |
| ProgressBar | 4 | 120 | 50 | -70 |
| Button | 5+ | 200 | 60 | -140 |
| ScreenHeader | 10+ | 300 | 80 | -220 |
| TabGroup | 3 | 150 | 70 | -80 |
| StatusBadge | 5+ | 100 | 40 | -60 |
| SearchInput | 3 | 60 | 30 | -30 |
| EmptyState | 4 | 80 | 35 | -45 |
| Common Styles | 15+ | 500 | 100 | -400 |
| Total | 40+ | 1,583 | 510 | -1,073 |
Additional Benefits:
- Type safety increased - 8 new TypeScript interfaces
- Consistency improved - Single source of truth for each pattern
- Maintainability improved - Changes propagate automatically
- Developer experience improved - Clear component APIs
Takeaway: Refactoring eliminated over 1,000 lines of duplicated code while adding well-structured reusable components. Code is more maintainable despite being functionally identical.
Common Questions (If You're Refactoring Your Codebase)
Q: Should I refactor working code?
A: Yes, if you're adding features. Refactor before adding new features to avoid propagating duplication. Don't refactor "just because"—have a reason (adding feature, fixing bugs, improving maintainability).
Q: How do I know when to extract a component?
A: Use the "three strike rule": First time inline, second time copy-paste, third time extract. Need at least two examples to identify the pattern.
Q: What if extracted component needs too many props?
A: Use variants for common cases + style prop for edge cases. If still too many props, component is doing too much—split into smaller components.
Q: Should I refactor everything at once?
A: No! Refactor gradually: Create component → Update one screen → Test → Repeat. Big-bang refactors risk breaking features.
Q: How do I handle component variations?
A: Three strategies:
-
Variants - Enumerated types (
variant?: 'primary' | 'danger') -
Boolean flags - Simple toggles (
showLabel?: boolean) -
Style overrides - Escape hatch (
style?: ViewStyle)
Q: What about performance of reusable components?
A: Negligible impact. React Native optimizes component rendering. Premature optimization is worse than duplication. Profile before optimizing.
Q: Should utilities be functions or hooks?
A: Functions for pure logic (color mappings, formatters). Hooks for stateful logic (data fetching, subscriptions). Status colors → function. API calls → hook.
Q: How do I document reusable components?
A: Three levels:
- JSDoc comments - On component and props
- Usage examples - In component file or Storybook
- Component library - Central documentation (later phase)
Q: What if team disagrees on abstractions?
A: Start with utilities (everyone agrees on shared colors). Then atoms (buttons, inputs). Then molecules. Build consensus gradually.
Q: How do I test refactored components?
A: Unit tests for components, integration tests for screens. Test component variations (props combinations). Test old screens still work.
Q: Should I extract every repeated pattern?
A: No. Extract when:
- Used 3+ times
- Logic is complex
- Changes affect multiple places
- Team agrees it's a pattern
Don't extract:
- One-off code
- Trivial JSX (like
<View style={{ marginTop: 10 }} />) - Still evolving patterns
Resources That Helped Me
- Atomic Design Methodology - Component hierarchy (atoms, molecules, organisms)
- React Component Patterns - Composition patterns
- TypeScript Generics - Type-safe generic components
- React Native StyleSheet - Performance of shared styles
- Refactoring UI - Design system principles
- Don't Repeat Yourself (DRY) - Classic principle
Code Repository
All the refactored code is available on GitHub:
- physio-care-react-native-first-project - Complete refactored codebase
Final Thoughts
Phase 7.5 was about stepping back from feature development and improving code quality. After building seven complete features, the codebase had accumulated duplication—not because of bad practices, but because patterns emerge over time. You can't identify patterns until you've written the code multiple times.
The biggest learning was understanding when to refactor. Not every duplication needs extraction. The "three strike rule" (first inline, second copy-paste, third extract) gave me confidence in my decisions. I wasn't prematurely abstracting; I was responding to proven patterns.
What surprised me most was how much TypeScript helped. Changing a component's prop structure and letting TypeScript find every usage was incredibly powerful. Without TypeScript, large refactors would be scary. With it, they're manageable.
The component hierarchy (atoms → molecules → organisms) provided mental models for organizing code. Atoms (Button, Avatar) have no dependencies. Molecules (ScreenHeader, TabGroup) compose atoms. This structure makes it obvious where new components belong.
Creating a design system (common styles, color tokens, spacing constants) had immediate benefits. Changing spacing globally or adding a new color variant became trivial. The design system grew organically from extracted patterns rather than being designed upfront.
The refactoring eliminated over 1,000 lines of duplicated code while adding clean, reusable components. The codebase is more maintainable, consistent, and ready for new features. But more importantly, I learned to recognize patterns and extract them systematically.
Next up: Building more features on this solid foundation and seeing how the component library grows (Phase 8).
If you're a React developer with a growing codebase, regular refactoring sessions are essential. Don't wait until duplication is overwhelming. Extract components gradually, one pattern at a time. Your future self (and your team) will thank you.
Top comments (0)