DEV Community

Cover image for Phase 4: Building the Appointments Flow - Forms, Dropdowns, and Conditional UI
Md Enayetur Rahman
Md Enayetur Rahman

Posted on

Phase 4: Building the Appointments Flow - Forms, Dropdowns, and Conditional UI

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:

  1. Session type selector with In-person/Online toggle
  2. Center selection dropdown with mock data
  3. Consultant selection with search functionality
  4. Conditional UI states (no center selected, no consultants available)
  5. Booking fee message display
  6. Video consultation screen with date and time slot selection
  7. 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
Enter fullscreen mode Exit fullscreen mode

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

Key Differences:

  1. Separate screen and molecule components: Complex forms broken into reusable pieces
  2. Type definitions for appointments: TypeScript types for centers, consultants, time slots
  3. 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",
  },
];
Enter fullscreen mode Exit fullscreen mode

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

Benefits of These Types:

  1. Form validation: TypeScript ensures you pass correct data types
  2. Autocomplete: IDE suggests available properties
  3. Relationships: centerId links consultants to centers
  4. Conditional logic: available boolean 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>
  );
}
Enter fullscreen mode Exit fullscreen mode
/* 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;
}
Enter fullscreen mode Exit fullscreen mode

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

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>
  );
}
Enter fullscreen mode Exit fullscreen mode
/* CenterSelector.css */
select {
  width: 100%;
  padding: 16px;
  border-radius: 8px;
  border: 1px solid #e5e5ea;
  background-color: #fff;
  font-size: 16px;
}
Enter fullscreen mode Exit fullscreen mode

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

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:

  1. Controlled visibility: Use showDropdown state to control when list appears
  2. FlatList for items: Efficient rendering, especially for long lists
  3. Touchable trigger: Entire selector area is touchable
  4. Close on select: Set showDropdown to 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

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:

  1. No center selected: Disable consultant selection
  2. No consultants available: Show "not available" message
  3. Search with no results: Show "no results for {query}"
  4. 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;
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

Why It Happened:

// ❌ BAD: FlatList inside ScrollView (both vertical)
<ScrollView>
  <SessionTypeSelector />
  <LocationSelector>
    <FlatList data={centers} /> {/* Nested FlatList! */}
  </LocationSelector>
</ScrollView>
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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:

  1. Native appearance: Looks like iOS/Android system alerts
  2. Multiple buttons: Can add Cancel, OK, custom buttons
  3. Callbacks: Each button can have its own action
  4. 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:

  1. Initially used FlatList as outer container with ListHeaderComponent
  2. 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>
  ));
}
Enter fullscreen mode Exit fullscreen mode

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

Better Solution: Use onTouchStart on container to close dropdowns when touching outside:

<View
  style={styles.container}
  onTouchStart={() => {
    setShowCenterDropdown(false);
    setShowConsultantDropdown(false);
  }}
>
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

  1. No Native Select Element: React Native doesn't have <select>. You build custom dropdowns with TouchableOpacity + FlatList (or map() for small lists).

  2. Conditional UI is Critical: Forms need clear states (disabled, empty, error). Good UX requires showing helpful messages at each state.

  3. TypeScript for Complex Forms: Type definitions prevent bugs when passing data between screens. Navigation types catch errors at compile time.

  4. State Dependencies Matter: When form fields depend on each other (center → consultants), always reset dependent fields when parent changes.

  5. VirtualizedList Performance: Don't nest FlatLists inside ScrollViews. For small lists, use map(). For large content, make FlatList the outer container.

  6. Search is Simple: Filtering lists in React Native is identical to web—just use .filter() with includes().

  7. Alert API is Powerful: React Native's Alert is better than web's alert()—native styling, multiple buttons, callbacks.

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


Code Repository

All the code from Phase 4 is available on GitHub:


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)