The Journey Continues
In Phase 3, I built the Home experience with goals, progress tracking, and a simple chart. Now it's time to tackle something more complex—a full appointment booking flow with session types, center selection, consultant search, and video consultation scheduling.
Coming from React web development, I was curious: How do you handle complex forms in React Native? How do dropdowns work without HTML <select>? Can you build conditional UI flows? Let me walk you through what I learned, comparing everything to what you already know from React.
What We're Building
In Phase 4, I implemented:
- Session type selector with In-person/Online toggle
- Center selection dropdown with mock data
- Consultant selection with search functionality
- Conditional UI states (no center selected, no consultants available)
- Booking fee message display
- Video consultation screen with date and time slot selection
- Navigation between booking flows
Everything follows the designs from the roadmap (SA-12 → SA-18, SA-27) and uses mock data to simulate real backend behavior.
Step 1: Project Structure - Adding Appointment Types
React (Web) - Typical Form Structure:
my-react-app/
├── src/
│ ├── components/
│ │ ├── BookingForm.js
│ │ └── ConsultantSelect.js
│ └── data/
│ └── consultants.js
React Native (What I Built):
physio-care/
├── src/
│ ├── components/
│ │ ├── screens/
│ │ │ ├── BookAppointmentScreen.tsx
│ │ │ └── VideoConsultationScreen.tsx
│ │ └── ui-molecules/
│ │ ├── SessionTypeSelector.tsx
│ │ └── LocationSelector.tsx
│ ├── data/
│ │ └── mockAppointments.ts
│ └── types/
│ └── appointment.ts
Key Differences:
- Separate screen and molecule components: Complex forms broken into reusable pieces
- Type definitions for appointments: TypeScript types for centers, consultants, time slots
- Mock appointment data: Separate file simulating backend responses
Takeaway: React Native encourages more granular component separation for complex UIs, especially for forms with multiple states.
Step 2: TypeScript Types for Appointments
This was crucial for managing form state! Let me show you the appointment types I created.
React (Web) - JavaScript Objects:
// consultants.js
export const consultants = [
{
id: "1",
name: "Dr. Sarah Johnson",
specialty: "Sports Physiotherapy",
experience: "8 years",
rating: 4.8,
centerId: "1",
},
];
React Native - TypeScript Types:
// src/types/appointment.ts
export type SessionType = "in-person" | "online";
export interface Center {
id: string;
name: string;
address: string;
city: string;
}
export interface Consultant {
id: string;
name: string;
specialty: string;
experience: string;
rating: number;
centerId: string;
}
export interface TimeSlot {
id: string;
time: string;
available: boolean;
}
export interface DateSlot {
date: string;
dayName: string;
dayNumber: string;
month: string;
slots: TimeSlot[];
}
Benefits of These Types:
- Form validation: TypeScript ensures you pass correct data types
- Autocomplete: IDE suggests available properties
-
Relationships:
centerIdlinks consultants to centers -
Conditional logic:
availableboolean controls UI states
Takeaway: Well-defined types make complex forms much easier to manage, especially when dealing with relationships (centers → consultants).
Step 3: Building the Session Type Toggle
React (Web) - Radio Buttons or Tabs:
// SessionTypeSelector.js
function SessionTypeSelector({ value, onChange }) {
return (
<div className="session-type-selector">
<button
className={value === "in-person" ? "active" : ""}
onClick={() => onChange("in-person")}
>
In-person
</button>
<button
className={value === "online" ? "active" : ""}
onClick={() => onChange("online")}
>
Online
</button>
</div>
);
}
/* SessionTypeSelector.css */
.session-type-selector {
display: flex;
gap: 12px;
margin-bottom: 24px;
}
.session-type-selector button {
flex: 1;
padding: 12px 16px;
border-radius: 8px;
border: 1px solid #e5e5ea;
background-color: #fff;
cursor: pointer;
}
.session-type-selector button.active {
background-color: #007aff;
color: white;
border-color: #007aff;
}
React Native - TouchableOpacity Toggle:
// src/components/ui-molecules/SessionTypeSelector.tsx
import React from "react";
import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
import { SessionType } from "../../types/appointment";
interface SessionTypeSelectorProps {
selectedType: SessionType;
onTypeChange: (type: SessionType) => void;
}
export default function SessionTypeSelector({
selectedType,
onTypeChange,
}: SessionTypeSelectorProps) {
return (
<View style={styles.container}>
<Text style={styles.label}>Session Type</Text>
<View style={styles.buttonGroup}>
<TouchableOpacity
style={[
styles.button,
selectedType === "in-person" && styles.activeButton,
]}
onPress={() => onTypeChange("in-person")}
>
<Text
style={[
styles.buttonText,
selectedType === "in-person" && styles.activeButtonText,
]}
>
In-person
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.button,
selectedType === "online" && styles.activeButton,
]}
onPress={() => onTypeChange("online")}
>
<Text
style={[
styles.buttonText,
selectedType === "online" && styles.activeButtonText,
]}
>
Online
</Text>
</TouchableOpacity>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
marginBottom: 24,
},
label: {
fontSize: 16,
fontWeight: "600",
color: "#333",
marginBottom: 12,
paddingHorizontal: 20,
},
buttonGroup: {
flexDirection: "row",
paddingHorizontal: 20,
gap: 12,
},
button: {
flex: 1,
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 8,
backgroundColor: "#fff",
alignItems: "center",
borderWidth: 1,
borderColor: "#E5E5EA",
},
activeButton: {
backgroundColor: "#007AFF",
borderColor: "#007AFF",
},
buttonText: {
fontSize: 14,
fontWeight: "600",
color: "#666",
},
activeButtonText: {
color: "#fff",
},
});
Key Differences:
| React Web | React Native |
|---|---|
<button> element |
<TouchableOpacity> |
onClick handler |
onPress handler |
className conditional |
style array conditional |
| CSS transitions (optional) | No built-in transitions |
cursor: pointer |
Not needed (touch by default) |
Takeaway: Toggle buttons work the same way conceptually. The main difference is using TouchableOpacity with style arrays instead of CSS classes.
Step 4: Building Custom Dropdowns (The Tricky Part!)
This was the biggest challenge! React Native doesn't have a native <select> element. You have to build custom dropdowns.
React (Web) - HTML Select:
// CenterSelector.js
function CenterSelector({ value, onChange, centers }) {
return (
<div className="selector">
<label>Select Center</label>
<select value={value} onChange={(e) => onChange(e.target.value)}>
<option value="">Choose a center...</option>
{centers.map((center) => (
<option key={center.id} value={center.id}>
{center.name}
</option>
))}
</select>
</div>
);
}
/* CenterSelector.css */
select {
width: 100%;
padding: 16px;
border-radius: 8px;
border: 1px solid #e5e5ea;
background-color: #fff;
font-size: 16px;
}
React Native - Custom Dropdown Component:
// src/components/ui-molecules/LocationSelector.tsx (simplified for clarity)
import React, { useState } from "react";
import {
View,
Text,
TouchableOpacity,
FlatList,
StyleSheet,
} from "react-native";
export default function LocationSelector({ selectedCenter, onCenterChange }) {
const [showDropdown, setShowDropdown] = useState(false);
const handleSelect = (center) => {
onCenterChange(center);
setShowDropdown(false);
};
return (
<View style={styles.section}>
<Text style={styles.label}>Select Center</Text>
{/* Dropdown trigger */}
<TouchableOpacity
style={styles.selector}
onPress={() => setShowDropdown(!showDropdown)}
>
<Text
style={selectedCenter ? styles.selectedText : styles.placeholderText}
>
{selectedCenter ? selectedCenter.name : "Choose a center..."}
</Text>
<Text style={styles.arrow}>{showDropdown ? "▲" : "▼"}</Text>
</TouchableOpacity>
{/* Dropdown list */}
{showDropdown && (
<View style={styles.dropdown}>
<FlatList
data={mockCenters}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<TouchableOpacity
style={styles.dropdownItem}
onPress={() => handleSelect(item)}
>
<Text style={styles.itemTitle}>{item.name}</Text>
<Text style={styles.itemSubtitle}>
{item.address}, {item.city}
</Text>
</TouchableOpacity>
)}
/>
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
selector: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
backgroundColor: "#fff",
borderRadius: 8,
padding: 16,
marginHorizontal: 20,
borderWidth: 1,
borderColor: "#E5E5EA",
},
dropdown: {
backgroundColor: "#fff",
borderRadius: 8,
marginHorizontal: 20,
borderWidth: 1,
borderColor: "#E5E5EA",
maxHeight: 200,
},
dropdownItem: {
padding: 16,
borderBottomWidth: 1,
borderBottomColor: "#F0F0F0",
},
});
Key Differences:
| React Web | React Native |
|---|---|
Native <select> element |
Custom-built with TouchableOpacity + state |
<option> elements |
FlatList with TouchableOpacity items |
| Browser handles dropdown | You manage visibility with state |
| Browser handles z-index | You control positioning with styles |
| Simple to implement | More complex but fully customizable |
Important Concepts:
-
Controlled visibility: Use
showDropdownstate to control when list appears - FlatList for items: Efficient rendering, especially for long lists
- Touchable trigger: Entire selector area is touchable
-
Close on select: Set
showDropdownto false after selection
Takeaway: Building custom dropdowns in React Native requires more code but gives you complete control over UX. It's like building a custom dropdown in web with HTML/CSS/JS.
Step 5: Adding Search to Consultants List
React (Web) - Filtered Select:
// ConsultantSelector.js
function ConsultantSelector({ consultants, onSelect }) {
const [search, setSearch] = useState("");
const filtered = consultants.filter((c) =>
c.name.toLowerCase().includes(search.toLowerCase())
);
return (
<div>
<input
type="text"
placeholder="Search consultants..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<select>
{filtered.map((c) => (
<option key={c.id}>{c.name}</option>
))}
</select>
</div>
);
}
React Native - Search with Filtered FlatList:
// Inside LocationSelector component
const [searchQuery, setSearchQuery] = useState("");
const availableConsultants = useMemo(() => {
if (!selectedCenter) return [];
let consultants = mockConsultants.filter(
(c) => c.centerId === selectedCenter.id
);
if (searchQuery.trim()) {
consultants = consultants.filter(
(c) =>
c.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
c.specialty.toLowerCase().includes(searchQuery.toLowerCase())
);
}
return consultants;
}, [selectedCenter, searchQuery]);
return (
<View>
<TextInput
style={styles.searchInput}
placeholder="Search consultants..."
value={searchQuery}
onChangeText={setSearchQuery}
/>
<FlatList
data={availableConsultants}
renderItem={({ item }) => (
<TouchableOpacity onPress={() => handleSelect(item)}>
<Text>{item.name}</Text>
<Text>{item.specialty}</Text>
</TouchableOpacity>
)}
/>
</View>
);
Key Differences:
| React Web | React Native |
|---|---|
<input type="text"> |
<TextInput> |
onChange event |
onChangeText prop |
e.target.value |
Value passed directly to callback |
useMemo (optional) |
useMemo (recommended) |
Performance Tip:
Using useMemo prevents recalculating filtered consultants on every render. This is especially important in React Native where performance matters more.
Takeaway: Search filtering works identically in logic. The main difference is TextInput vs <input> and slightly different event handling.
Step 6: Conditional UI States
This was important for UX! You need to show different messages based on state.
React (Web) - Conditional Rendering:
// BookingForm.js
function BookingForm() {
const [center, setCenter] = useState(null);
const [consultant, setConsultant] = useState(null);
const consultants = center ? getConsultants(center.id) : [];
return (
<div>
<CenterSelector value={center} onChange={setCenter} />
{!center ? (
<p className="disabled-message">Please select a center first</p>
) : consultants.length === 0 ? (
<p className="error-message">No consultants available</p>
) : (
<ConsultantSelector
consultants={consultants}
onChange={setConsultant}
/>
)}
{center && consultant && (
<div className="fee-message">
We charge INR 100 to ensure your booking...
</div>
)}
</div>
);
}
React Native - Same Logic, Different Components:
// In LocationSelector component
{
selectedCenter ? (
<>
<TextInput
style={styles.searchInput}
placeholder="Search consultants..."
value={searchQuery}
onChangeText={setSearchQuery}
/>
{availableConsultants.length > 0 ? (
<FlatList
data={availableConsultants}
renderItem={renderConsultantItem}
/>
) : (
<View style={styles.noConsultants}>
<Text style={styles.noConsultantsText}>
No consultants available for "{searchQuery}"
</Text>
</View>
)}
</>
) : (
<View style={styles.disabledSection}>
<Text style={styles.disabledText}>Please select a center first</Text>
</View>
);
}
{
selectedCenter && selectedConsultant && (
<View style={styles.feeMessage}>
<Text style={styles.feeText}>
We charge INR 100 to ensure your booking. It will be adjusted in your
final payment.
</Text>
</View>
);
}
Key Differences:
| React Web | React Native |
|---|---|
<p> for messages |
<View> + <Text>
|
| CSS classes for states | StyleSheet styles |
| Same conditional logic | Same conditional logic |
States I Handled:
- No center selected: Disable consultant selection
- No consultants available: Show "not available" message
- Search with no results: Show "no results for {query}"
- Both selected: Show booking fee message
Takeaway: Conditional rendering logic is identical in React Native. The only difference is the components you render (View/Text vs div/p).
Step 7: Nested Navigation - Video Consultation Screen
React (Web) - Route with State:
// App.js
<Routes>
<Route path="/book-appointment" element={<BookAppointment />} />
<Route path="/video-consultation" element={<VideoConsultation />} />
</Routes>;
// BookAppointment.js
function BookAppointment() {
const navigate = useNavigate();
const handleProceed = () => {
navigate("/video-consultation", {
state: { center, consultant, sessionType },
});
};
}
// VideoConsultation.js
function VideoConsultation() {
const location = useLocation();
const { center, consultant } = location.state;
}
React Native - Typed Navigation with Params:
// Update types first
// src/types/navigation.ts
export type HomeStackParamList = {
HomeMain: undefined;
GoalDetails: { goalId: string };
BookAppointment: undefined;
VideoConsultation: {
center: Center;
consultant: Consultant;
sessionType: SessionType;
};
};
// BookAppointmentScreen.tsx
const navigation = useNavigation<HomeStackNavigationProp>();
const handleProceed = () => {
navigation.navigate("VideoConsultation", {
center: selectedCenter,
consultant: selectedConsultant,
sessionType,
});
};
// VideoConsultationScreen.tsx
const route = useRoute();
const { center, consultant, sessionType } =
route.params as VideoConsultationParams;
Key Differences:
| React Web | React Native |
|---|---|
useNavigate() hook |
navigation from useNavigation()
|
location.state for params |
route.params for params |
| TypeScript types optional | Types required for param safety |
| URL-based routing | Screen-based routing |
Benefit of Typed Navigation:
TypeScript ensures you pass the correct parameters. If you forget to pass center, you'll get a compile error!
Takeaway: React Navigation's typed params are more type-safe than React Router's location state. The compiler catches mistakes before runtime.
Step 8: Date and Time Slot Selection
React (Web) - Calendar and Time Picker:
// VideoConsultation.js
function VideoConsultation() {
const [selectedDate, setSelectedDate] = useState(null);
const [selectedTime, setSelectedTime] = useState(null);
return (
<div>
<input
type="date"
value={selectedDate}
onChange={(e) => setSelectedDate(e.target.value)}
/>
<div className="time-slots">
{timeSlots.map((slot) => (
<button
key={slot.id}
className={selectedTime === slot.id ? "selected" : ""}
onClick={() => setSelectedTime(slot.id)}
disabled={!slot.available}
>
{slot.time}
</button>
))}
</div>
</div>
);
}
React Native - Custom Date Picker and Time Grid:
// VideoConsultationScreen.tsx
const [selectedDate, setSelectedDate] = useState<string | null>(null);
const [selectedTimeSlot, setSelectedTimeSlot] = useState<TimeSlot | null>(null);
return (
<ScrollView>
{/* Date Selection - Horizontal Scroll */}
<FlatList
data={mockDateSlots}
keyExtractor={(item) => item.date}
renderItem={({ item }) => (
<TouchableOpacity
style={[
styles.dateCard,
selectedDate === item.date && styles.selectedDateCard,
]}
onPress={() => {
setSelectedDate(item.date);
setSelectedTimeSlot(null); // Reset time when date changes
}}
>
<Text style={styles.dayName}>{item.dayName}</Text>
<Text style={styles.dayNumber}>{item.dayNumber}</Text>
<Text style={styles.month}>{item.month}</Text>
</TouchableOpacity>
)}
horizontal
showsHorizontalScrollIndicator={false}
/>
{/* Time Slots - Grid Layout */}
{selectedDateData && (
<View style={styles.timeSlotsGrid}>
{selectedDateData.slots.map((slot) => (
<TouchableOpacity
key={slot.id}
style={[
styles.timeSlot,
!slot.available && styles.unavailableSlot,
selectedTimeSlot?.id === slot.id && styles.selectedTimeSlot,
]}
onPress={() => slot.available && setSelectedTimeSlot(slot)}
disabled={!slot.available}
>
<Text
style={[
styles.timeText,
!slot.available && styles.unavailableText,
selectedTimeSlot?.id === slot.id && styles.selectedTimeText,
]}
>
{slot.time}
</Text>
</TouchableOpacity>
))}
</View>
)}
</ScrollView>
);
Key Differences:
| React Web | React Native |
|---|---|
<input type="date"> |
Custom date cards with FlatList
|
| Native date picker | Horizontal scroll of date cards |
| CSS Grid for time slots | flexWrap for grid layout |
disabled attribute |
disabled prop + style |
Layout Trick - flexWrap for Grid:
const styles = StyleSheet.create({
timeSlotsGrid: {
flexDirection: "row",
flexWrap: "wrap", // Creates grid effect
paddingHorizontal: 20,
gap: 12,
},
timeSlot: {
minWidth: 100, // Controls column width
},
});
Takeaway: React Native doesn't have native date/time pickers (without libraries). You build custom UIs, which gives you complete design control but requires more code.
Step 9: Fixing the VirtualizedList Warning
This was an important learning moment! I hit a console error.
The Problem:
VirtualizedLists should never be nested inside plain ScrollViews
with the same orientation - use another VirtualizedList-backed
container instead.
Why It Happened:
// ❌ BAD: FlatList inside ScrollView (both vertical)
<ScrollView>
<SessionTypeSelector />
<LocationSelector>
<FlatList data={centers} /> {/* Nested FlatList! */}
</LocationSelector>
</ScrollView>
The Solution (Option 1): Remove FlatList for Small Lists
Since my centers/consultants lists are small (mock data), I replaced FlatList with simple mapping:
// ✅ GOOD: No FlatList, just map()
{
mockCenters.map((item) => (
<TouchableOpacity key={item.id} onPress={() => handleSelect(item)}>
<Text>{item.name}</Text>
</TouchableOpacity>
));
}
The Solution (Option 2): Replace ScrollView with FlatList
For larger content, make the outer container a FlatList:
// ✅ GOOD: FlatList as outer container
<FlatList
data={[{ key: "content" }]}
renderItem={() => null}
ListHeaderComponent={MainContent}
ListFooterComponent={Footer}
/>
When to Use Each:
| Solution | When to Use |
|---|---|
| Remove FlatList | Small, fixed lists (< 20 items) |
| FlatList container | Large content with nested scrollable sections |
Takeaway: React Native is strict about scroll performance. Nested vertical scrolls break virtualization. For small lists, mapping is simpler than FlatList.
Step 10: Form Validation and Alerts
React (Web) - Form Validation:
// BookingForm.js
function BookingForm() {
const handleSubmit = (e) => {
e.preventDefault();
if (!center || !consultant) {
alert("Please select both center and consultant");
return;
}
// Proceed with booking
};
return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
<button type="submit">Book Appointment</button>
</form>
);
}
React Native - Alert API:
// BookAppointmentScreen.tsx
import { Alert } from "react-native";
const handleProceedToBooking = () => {
if (!selectedCenter || !selectedConsultant) {
Alert.alert(
"Selection Required",
"Please select both a center and consultant to proceed.",
[{ text: "OK" }]
);
return;
}
Alert.alert(
"Booking Confirmed",
`Your appointment with ${selectedConsultant.name} has been booked!`,
[{ text: "OK", onPress: () => navigation.goBack() }]
);
};
Key Differences:
| React Web | React Native |
|---|---|
alert() function |
Alert.alert() API |
| Single string | Title + Message + Buttons |
| OK button only | Customizable buttons with callbacks |
| Browser-styled | Native platform-styled |
Alert.alert Benefits:
- Native appearance: Looks like iOS/Android system alerts
- Multiple buttons: Can add Cancel, OK, custom buttons
- Callbacks: Each button can have its own action
-
Better UX: More professional than web
alert()
Takeaway: React Native's Alert API is more powerful than web's alert(). It's styled natively and supports multiple buttons with callbacks.
Problems I Faced & How I Solved Them
Problem 1: VirtualizedList Warning
Issue: Console showed "VirtualizedLists should never be nested..." warning.
Root Cause: I had FlatLists (centers and consultants) inside a vertical ScrollView in BookAppointmentScreen.
Solution:
- Initially used FlatList as outer container with ListHeaderComponent
- Later simplified by removing FlatList from LocationSelector since lists were small
// ✅ Final solution: Simple map() for small lists
{
mockCenters.map((item) => (
<TouchableOpacity key={item.id}>
<Text>{item.name}</Text>
</TouchableOpacity>
));
}
Takeaway: For mock data with < 20 items, map() is simpler than FlatList. Save FlatList for large, dynamic lists from APIs.
Problem 2: Dropdown Closing When Scrolling
Issue: When I scrolled the main screen, dropdowns stayed open and looked broken.
Solution: Close dropdowns when user interacts with other parts of screen:
// Add to ScrollView
<ScrollView
onScroll={() => {
setShowCenterDropdown(false);
setShowConsultantDropdown(false);
}}
scrollEventThrottle={16}
>
Better Solution: Use onTouchStart on container to close dropdowns when touching outside:
<View
style={styles.container}
onTouchStart={() => {
setShowCenterDropdown(false);
setShowConsultantDropdown(false);
}}
>
Takeaway: Custom dropdowns require manual state management. Always close dropdowns when focus moves elsewhere.
Problem 3: TypeScript Errors with Navigation Params
Issue: TypeScript complained about passing objects through navigation:
Type 'Center' is not assignable to type 'undefined'
Root Cause: Navigation types weren't properly defined.
Solution: Import types in navigation definition:
// src/types/navigation.ts
export type HomeStackParamList = {
VideoConsultation: {
center: import("./appointment").Center; // Import type here
consultant: import("./appointment").Consultant;
sessionType: import("./appointment").SessionType;
};
};
Takeaway: When passing complex objects through navigation, use import() syntax in type definitions to avoid circular dependencies.
Problem 4: Search Not Updating Dropdown
Issue: Typing in search input didn't update the consultant list.
Root Cause: Forgot to use useMemo for filtered list, causing stale data.
Solution:
// ✅ Use useMemo to recalculate when dependencies change
const availableConsultants = useMemo(() => {
let consultants = mockConsultants.filter(
(c) => c.centerId === selectedCenter?.id
);
if (searchQuery.trim()) {
consultants = consultants.filter((c) =>
c.name.toLowerCase().includes(searchQuery.toLowerCase())
);
}
return consultants;
}, [selectedCenter, searchQuery]); // Recalculate when these change
Takeaway: Use useMemo for derived data that depends on multiple state variables. It ensures data stays in sync and improves performance.
Problem 5: Time Slots Not Resetting When Date Changes
Issue: Selected time slot stayed highlighted when changing dates, even though it wasn't valid for new date.
Solution: Reset time slot when date changes:
const handleDateSelect = (date: string) => {
setSelectedDate(date);
setSelectedTimeSlot(null); // Reset time slot
};
Takeaway: When form fields depend on each other, always reset dependent fields when parent changes (date → time, center → consultant, etc.).
What I Tested
Here's my testing checklist for Phase 4:
✅ Session Type Selection
- [x] "In-person" selected by default
- [x] Can switch to "Online" and back
- [x] Visual highlighting works correctly
- [x] State persists during form interaction
✅ Center Selection
- [x] Dropdown opens when tapped
- [x] Shows all available centers
- [x] Displays center name, address, and city
- [x] Closes when center is selected
- [x] Selected center displays correctly
- [x] Closes when tapping elsewhere
✅ Consultant Selection
- [x] Disabled until center is selected
- [x] Shows message "Please select a center first"
- [x] Displays consultants for selected center only
- [x] Search input filters consultants by name
- [x] Search filters by specialty too
- [x] Shows "No consultants available" when filtered list is empty
- [x] Rating displays correctly
- [x] Resets when center changes
✅ Booking Fee Message
- [x] Only shows when both center and consultant selected
- [x] Message is clear and readable
- [x] Styled with distinct background color
✅ Form Validation
- [x] "Book Appointment" button disabled when selections incomplete
- [x] Alert shows when trying to proceed without selections
- [x] Alert message is clear and helpful
- [x] Success alert shows after valid booking
- [x] Navigation back works after confirmation
✅ Video Consultation Flow
- [x] Navigates from BookAppointment when "Online" selected
- [x] Consultant info displays correctly
- [x] Date cards scroll horizontally
- [x] Can select a date
- [x] Time slots appear after date selection
- [x] Time slot selection works
- [x] Unavailable slots are disabled and styled differently
- [x] Selected slots highlight correctly
- [x] Time slot resets when date changes
- [x] Confirm button disabled until both date and time selected
- [x] Success alert shows after booking
✅ Navigation
- [x] Back button works on all screens
- [x] Navigation from Home → BookAppointment works
- [x] Navigation from BookAppointment → VideoConsultation works
- [x] Params pass correctly between screens
- [x] No navigation errors or warnings
✅ Edge Cases
- [x] Handles empty consultant list gracefully
- [x] Search with no results shows helpful message
- [x] Dropdowns close when scrolling main content
- [x] No VirtualizedList warnings in console
- [x] Works on different screen sizes (tested on multiple emulators)
What I Learned: Key Takeaways
No Native Select Element: React Native doesn't have
<select>. You build custom dropdowns with TouchableOpacity + FlatList (or map() for small lists).Conditional UI is Critical: Forms need clear states (disabled, empty, error). Good UX requires showing helpful messages at each state.
TypeScript for Complex Forms: Type definitions prevent bugs when passing data between screens. Navigation types catch errors at compile time.
State Dependencies Matter: When form fields depend on each other (center → consultants), always reset dependent fields when parent changes.
VirtualizedList Performance: Don't nest FlatLists inside ScrollViews. For small lists, use map(). For large content, make FlatList the outer container.
Search is Simple: Filtering lists in React Native is identical to web—just use
.filter()withincludes().Alert API is Powerful: React Native's Alert is better than web's alert()—native styling, multiple buttons, callbacks.
Custom Date Pickers: Building custom date/time selectors gives full design control but requires more code than web's native inputs.
Common Questions (If You're Coming from React)
Q: Can I use a library for dropdowns instead of building custom ones?
A: Yes! react-native-picker-select and react-native-dropdown-picker are popular. I built custom ones to learn, but libraries are fine for production.
Q: What about date pickers? Are there libraries?
A: Yes! @react-native-community/datetimepicker provides native date/time pickers. I built a custom UI for design consistency.
Q: How do I handle keyboard covering inputs?
A: Use KeyboardAvoidingView wrapper. I'll cover this in detail when we add more complex forms in Phase 7.
Q: Can I use form libraries like Formik or React Hook Form?
A: Absolutely! Both work great with React Native. I used plain state for learning, but form libraries are recommended for complex forms.
Q: How do I validate forms properly?
A: Same as web! Use libraries like Yup for validation schemas, or write custom validation functions. The logic is identical.
Q: Should I use FlatList or map() for lists?
A: FlatList for large lists (100+ items) from APIs. map() for small, fixed lists (< 50 items). FlatList virtualizes; map() renders everything.
Resources That Helped Me
- React Navigation - Passing parameters - Great guide on typed navigation
- React Native - FlatList - Official FlatList documentation
- React Native - TextInput - Input handling and keyboard behavior
- React Native - Alert - Alert API documentation
- useMemo Hook - Optimizing expensive calculations
Code Repository
All the code from Phase 4 is available on GitHub:
- physio-care-react-native-first-project - Complete source code
Final Thoughts
Phase 4 was challenging but incredibly rewarding. Building a full appointment booking flow taught me how to handle complex forms in React Native—custom dropdowns, search filtering, conditional UI, and navigation with parameters.
The biggest learning was understanding that React Native gives you lower-level building blocks. You don't get a <select> element, but you get the primitives to build any dropdown you can imagine. This extra control means more code upfront but infinite flexibility.
Next up: Building the Timeline view (Phase 5) with appointment history, session cards, and progress tracking. This will introduce SectionList, bottom sheets, and more advanced list patterns.
If you're a React developer, the transition to React Native for forms might feel verbose at first. But once you build a few custom components (dropdowns, date pickers), you'll have a reusable library that works exactly how you want.
Top comments (0)