DEV Community

Cover image for Phase 2: Building Navigation & Auth Flow in React Native
Md Enayetur Rahman
Md Enayetur Rahman

Posted on

Phase 2: Building Navigation & Auth Flow in React Native

The Journey Continues

In Phase 1, I set up my React Native development environment and created my first "Hello World" app. Now it's time to build something real—an authentication flow with navigation.

Coming from React web development, I was curious: How different would navigation be? How do forms work? What about styling? Let me walk you through what I learned, comparing everything to what you already know from React.


What We're Building

In Phase 2, I implemented:

  1. Project folder structure (organizing code like a pro)
  2. Navigation system (Auth stack + Main tabs)
  3. OTP-based login flow (Login → OTP → Profile Setup)
  4. Context API for auth state (managing user login state)

This is the foundation that will let me build the rest of the app features in future phases.


Step 1: Project Structure - React vs React Native

React (Web) - Typical Structure:

my-react-app/
├── src/
│   ├── components/
│   │   ├── Header.js
│   │   └── Footer.js
│   ├── pages/
│   │   ├── Home.js
│   │   └── About.js
│   ├── App.js
│   └── index.js
├── public/
└── package.json
Enter fullscreen mode Exit fullscreen mode

React Native (What I Built):

physio-care/
├── src/
│   ├── components/
│   │   ├── screens/          # Full-screen components
│   │   │   ├── LoginScreen.tsx
│   │   │   ├── OTPScreen.tsx
│   │   │   └── UserDetailsScreen.tsx
│   │   └── ui-molecules/    # Reusable UI components
│   ├── navigation/          # Navigation configuration
│   │   ├── RootNavigator.tsx
│   │   ├── AuthStackNavigator.tsx
│   │   └── MainTabNavigator.tsx
│   ├── context/             # Context providers
│   │   └── AuthContext.tsx
│   ├── types/               # TypeScript type definitions
│   │   ├── navigation.ts
│   │   └── user.ts
│   └── hooks/               # Custom hooks (for future use)
├── App.tsx
└── package.json
Enter fullscreen mode Exit fullscreen mode

Key Differences:

  1. screens/ folder: In React Native, we think in terms of "screens" (full-screen views) rather than "pages"
  2. navigation/ folder: Navigation is more complex in mobile apps, so it gets its own folder
  3. types/ folder: TypeScript types are crucial for navigation type safety (more on this later)
  4. No public/ folder: Mobile apps don't have static HTML files

Takeaway: The structure is similar, but mobile apps need more organization around navigation and screen management.


Step 2: Navigation - React Router vs React Navigation

This was the biggest learning curve for me. In React web, navigation is straightforward. In React Native, it's more structured.

React (Web) - React Router:

// App.js
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Login from "./pages/Login";
import Home from "./pages/Home";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/login" element={<Login />} />
        <Route path="/home" element={<Home />} />
      </Routes>
    </BrowserRouter>
  );
}
Enter fullscreen mode Exit fullscreen mode

Navigation:

import { useNavigate } from "react-router-dom";

function Login() {
  const navigate = useNavigate();

  const handleLogin = () => {
    navigate("/home");
  };
}
Enter fullscreen mode Exit fullscreen mode

React Native - React Navigation:

// App.tsx
import { NavigationContainer } from "@react-navigation/native";
import { AuthProvider } from "./src/context/AuthContext";
import RootNavigator from "./src/navigation/RootNavigator";

export default function App() {
  return (
    <AuthProvider>
      <NavigationContainer>
        <RootNavigator />
      </NavigationContainer>
    </AuthProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Navigation Structure:

// src/navigation/RootNavigator.tsx
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { useAuth } from "../context/AuthContext";
import AuthStackNavigator from "./AuthStackNavigator";
import MainTabNavigator from "./MainTabNavigator";

const Stack = createNativeStackNavigator();

export default function RootNavigator() {
  const { isLoggedIn } = useAuth();

  return (
    <Stack.Navigator>
      {isLoggedIn ? (
        <Stack.Screen name="Main" component={MainTabNavigator} />
      ) : (
        <Stack.Screen name="Auth" component={AuthStackNavigator} />
      )}
    </Stack.Navigator>
  );
}
Enter fullscreen mode Exit fullscreen mode

Navigation Between Screens:

// LoginScreen.tsx
import { AuthNavigationProp } from "../../types/navigation";

interface Props {
  navigation: AuthNavigationProp;
}

export default function LoginScreen({ navigation }: Props) {
  const handleSendOTP = () => {
    navigation.navigate("OTP", { mobile: "9876543210" });
  };
}
Enter fullscreen mode Exit fullscreen mode

Key Differences:

React Router (Web) React Navigation (Mobile)
BrowserRouter wraps app NavigationContainer wraps app
Routes and Route components Stack.Navigator and Stack.Screen
useNavigate() hook navigation prop passed to screens
URL-based routing (/login) Screen name-based ('Login')
Browser back button works automatically Native back button handled automatically

Takeaway: React Navigation is more structured and type-safe, but the concept is similar—you define routes and navigate between them.


Step 3: TypeScript Types for Navigation

This was new to me! React Navigation requires TypeScript types for type safety. In React Router, you don't need this.

Defining Navigation Types:

// src/types/navigation.ts
export type AuthStackParamList = {
  Login: undefined; // No params
  OTP: { mobile: string }; // Requires mobile param
  UserDetails: undefined;
};

export type MainTabParamList = {
  Home: undefined;
  Timeline: undefined;
  Support: undefined;
  Profile: undefined;
};

// Navigation prop types
export type AuthNavigationProp =
  import("@react-navigation/native-stack").NativeStackNavigationProp<AuthStackParamList>;
Enter fullscreen mode Exit fullscreen mode

Why This Matters:

  • Type Safety: TypeScript will catch navigation errors at compile time
  • Autocomplete: Your IDE will suggest available screen names
  • Param Validation: Ensures you pass the correct params to each screen

React Router Comparison:

In React Router, you might use:

navigate("/user/123"); // No type checking - could be wrong!
Enter fullscreen mode Exit fullscreen mode

In React Navigation with TypeScript:

navigation.navigate("OTP", { mobile: "9876543210" }); // TypeScript ensures mobile exists
Enter fullscreen mode Exit fullscreen mode

Takeaway: TypeScript types for navigation feel like extra work initially, but they prevent bugs and improve developer experience.


Step 4: Context API - Same Concept, Same Implementation

Good news! Context API works exactly the same in React Native as it does in React web.

React (Web) - Auth Context:

// AuthContext.js
import { createContext, useState, useContext } from "react";

const AuthContext = createContext();

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const isLoggedIn = user !== null;

  return (
    <AuthContext.Provider value={{ user, setUser, isLoggedIn }}>
      {children}
    </AuthContext.Provider>
  );
}

export const useAuth = () => useContext(AuthContext);
Enter fullscreen mode Exit fullscreen mode

React Native - Auth Context (Identical!):

// src/context/AuthContext.tsx
import { createContext, useState, useContext, ReactNode } from "react";
import { User } from "../types/user";

interface AuthContextType {
  user: User | null;
  setUser: (user: User | null) => void;
  isLoggedIn: boolean;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export const AuthProvider: React.FC<{ children: ReactNode }> = ({
  children,
}) => {
  const [user, setUser] = useState<User | null>(null);
  const isLoggedIn = user !== null;

  return (
    <AuthContext.Provider value={{ user, setUser, isLoggedIn }}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error("useAuth must be used within an AuthProvider");
  }
  return context;
};
Enter fullscreen mode Exit fullscreen mode

Usage (Same in Both):

// In any component
const { user, setUser, isLoggedIn } = useAuth();
Enter fullscreen mode Exit fullscreen mode

Takeaway: Context API is identical! If you know it in React, you know it in React Native. The only difference is TypeScript types (which are optional but recommended).


Step 5: Forms & Input Handling

Forms work similarly, but there are some mobile-specific differences.

React (Web) - Form Input:

function LoginForm() {
  const [mobile, setMobile] = useState("");

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="tel"
        value={mobile}
        onChange={(e) => setMobile(e.target.value)}
        placeholder="Mobile Number"
      />
      <button type="submit">Send OTP</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

React Native - Form Input:

// LoginScreen.tsx
import { TextInput, TouchableOpacity, Alert } from "react-native";

export default function LoginScreen({ navigation }: Props) {
  const [mobile, setMobile] = useState("");

  const handleSendOTP = () => {
    const cleanedMobile = mobile.replace(/\D/g, "");

    if (cleanedMobile.length !== 10) {
      Alert.alert(
        "Invalid Mobile",
        "Please enter a valid 10-digit mobile number"
      );
      return;
    }

    navigation.navigate("OTP", { mobile: cleanedMobile });
  };

  return (
    <View style={styles.container}>
      <TextInput
        style={styles.input}
        placeholder="Mobile Number"
        placeholderTextColor="#999"
        keyboardType="phone-pad" // Mobile-specific!
        value={mobile}
        onChangeText={setMobile} // Simpler than onChange
        maxLength={10}
      />
      <TouchableOpacity style={styles.button} onPress={handleSendOTP}>
        <Text style={styles.buttonText}>Send OTP</Text>
      </TouchableOpacity>
    </View>
  );
}
Enter fullscreen mode Exit fullscreen mode

Key Differences:

React Web React Native
<input> <TextInput>
<button> <TouchableOpacity> or <Button>
onChange={(e) => setValue(e.target.value)} onChangeText={setValue} (directly receives string)
type="tel" keyboardType="phone-pad" (shows numeric keyboard)
alert() Alert.alert() (native alert)
No maxLength needed maxLength prop available

Takeaway: Input handling is simpler in React Native (onChangeText directly gives you the value), but you need to use mobile-specific components and keyboard types.


Step 6: Styling - CSS vs StyleSheet

This is where things get really different! React Native doesn't use CSS—everything is JavaScript objects.

React (Web) - CSS Classes:

// App.js
import "./App.css";

function App() {
  return (
    <div className="container">
      <h1 className="title">Welcome</h1>
      <button className="btn-primary">Click Me</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
/* App.css */
.container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 20px;
}

.title {
  font-size: 28px;
  font-weight: bold;
  color: #333;
  margin-bottom: 8px;
}

.btn-primary {
  background-color: #007aff;
  color: white;
  padding: 16px;
  border-radius: 8px;
  border: none;
  cursor: pointer;
}
Enter fullscreen mode Exit fullscreen mode

React Native - StyleSheet:

// LoginScreen.tsx
import { StyleSheet, View, Text, TouchableOpacity } from "react-native";

export default function LoginScreen() {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>Welcome to PhysioCare</Text>
      <TouchableOpacity style={styles.button}>
        <Text style={styles.buttonText}>Send OTP</Text>
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1, // Takes full height
    backgroundColor: "#fff",
    padding: 20,
    justifyContent: "center", // Vertical centering
    alignItems: "center", // Horizontal centering
  },
  title: {
    fontSize: 28, // No 'px' needed
    fontWeight: "bold", // String, not number
    color: "#333",
    marginBottom: 8,
    textAlign: "center",
  },
  button: {
    backgroundColor: "#007AFF",
    borderRadius: 8,
    padding: 16,
    alignItems: "center", // Centers children
  },
  buttonText: {
    color: "#fff",
    fontSize: 16,
    fontWeight: "600",
  },
});
Enter fullscreen mode Exit fullscreen mode

Styling Comparison Table:

CSS (Web) React Native StyleSheet
display: flex Default (always flexbox)
flex-direction: column flexDirection: 'column' (camelCase)
justify-content: center justifyContent: 'center'
align-items: center alignItems: 'center'
font-size: 28px fontSize: 28 (no 'px')
font-weight: bold fontWeight: 'bold' (string)
background-color: #fff backgroundColor: '#fff' (camelCase)
border-radius: 8px borderRadius: 8
padding: 20px padding: 20
margin-bottom: 8px marginBottom: 8
text-align: center textAlign: 'center'

Key Styling Differences:

  1. No CSS Files: Everything is JavaScript objects
  2. CamelCase Properties: background-colorbackgroundColor
  3. No Units: Numbers are in logical pixels (no px, em, rem)
  4. Flexbox by Default: Every container is a flex container
  5. Limited Properties: No hover, :before, :after, etc.
  6. StyleSheet.create(): Optimizes styles (recommended but not required)

Inline Styles Work Too:

// You can use inline styles
<View style={{ padding: 20, backgroundColor: "#fff" }}>
  <Text style={{ fontSize: 18, color: "#333" }}>Hello</Text>
</View>
Enter fullscreen mode Exit fullscreen mode

But StyleSheet.create() is preferred because:

  • Better performance (styles are created once)
  • Better organization
  • Type checking (with TypeScript)

Takeaway: Styling in React Native is JavaScript objects instead of CSS. It's more limited but simpler and more predictable. Flexbox is your best friend!


Step 7: Building the Login Flow

Let me walk you through the complete login flow I built:

Screen 1: LoginScreen

// src/components/screens/LoginScreen.tsx
import React, { useState } from "react";
import {
  View,
  Text,
  TextInput,
  TouchableOpacity,
  StyleSheet,
  Alert,
} from "react-native";
import { AuthNavigationProp } from "../../types/navigation";

interface Props {
  navigation: AuthNavigationProp;
}

export default function LoginScreen({ navigation }: Props) {
  const [mobile, setMobile] = useState("");

  const handleSendOTP = () => {
    const cleanedMobile = mobile.replace(/\D/g, "");

    if (cleanedMobile.length !== 10) {
      Alert.alert(
        "Invalid Mobile",
        "Please enter a valid 10-digit mobile number"
      );
      return;
    }

    navigation.navigate("OTP", { mobile: cleanedMobile });
  };

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Welcome to PhysioCare</Text>
      <Text style={styles.subtitle}>Enter your mobile number to continue</Text>

      <View style={styles.inputContainer}>
        <TextInput
          style={styles.input}
          placeholder="Mobile Number"
          placeholderTextColor="#999"
          keyboardType="phone-pad"
          value={mobile}
          onChangeText={setMobile}
          maxLength={10}
        />
      </View>

      <TouchableOpacity style={styles.button} onPress={handleSendOTP}>
        <Text style={styles.buttonText}>Send OTP</Text>
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    padding: 20,
    justifyContent: "center",
  },
  title: {
    fontSize: 28,
    fontWeight: "bold",
    color: "#333",
    marginBottom: 8,
    textAlign: "center",
  },
  subtitle: {
    fontSize: 16,
    color: "#666",
    marginBottom: 40,
    textAlign: "center",
  },
  inputContainer: {
    marginBottom: 20,
  },
  input: {
    borderWidth: 1,
    borderColor: "#ddd",
    borderRadius: 8,
    padding: 16,
    fontSize: 16,
    backgroundColor: "#f9f9f9",
  },
  button: {
    backgroundColor: "#007AFF",
    borderRadius: 8,
    padding: 16,
    alignItems: "center",
  },
  buttonText: {
    color: "#fff",
    fontSize: 16,
    fontWeight: "600",
  },
});
Enter fullscreen mode Exit fullscreen mode

What's Happening:

  1. User enters mobile number
  2. On button press, we validate (10 digits)
  3. If valid, navigate to OTP screen with mobile number as param
  4. If invalid, show native alert

Screen 2: OTPScreen

// src/components/screens/OTPScreen.tsx
import React, { useState } from "react";
import {
  View,
  Text,
  TextInput,
  TouchableOpacity,
  StyleSheet,
  Alert,
} from "react-native";
import { RouteProp } from "@react-navigation/native";
import { AuthNavigationProp, AuthStackParamList } from "../../types/navigation";

interface Props {
  navigation: AuthNavigationProp;
  route: RouteProp<AuthStackParamList, "OTP">;
}

export default function OTPScreen({ navigation, route }: Props) {
  const { mobile } = route.params; // Get mobile from navigation params
  const [otp, setOtp] = useState("");

  const handleVerify = () => {
    const cleanedOtp = otp.replace(/\D/g, "");

    if (cleanedOtp.length < 4 || cleanedOtp.length > 6) {
      Alert.alert("Invalid OTP", "Please enter a valid OTP (4-6 digits)");
      return;
    }

    // Mock verification - in real app, call API here
    Alert.alert("OTP Verified", "Proceeding to fill details...", [
      {
        text: "OK",
        onPress: () => navigation.navigate("UserDetails"),
      },
    ]);
  };

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Enter OTP</Text>
      <Text style={styles.subtitle}>We sent an OTP to {mobile}</Text>

      <View style={styles.inputContainer}>
        <TextInput
          style={styles.input}
          placeholder="Enter OTP"
          placeholderTextColor="#999"
          keyboardType="number-pad"
          value={otp}
          onChangeText={setOtp}
          maxLength={6}
        />
      </View>

      <TouchableOpacity style={styles.button} onPress={handleVerify}>
        <Text style={styles.buttonText}>Verify OTP</Text>
      </TouchableOpacity>
    </View>
  );
}
Enter fullscreen mode Exit fullscreen mode

Key Points:

  • route.params gives you the data passed from previous screen
  • TypeScript ensures mobile exists (from AuthStackParamList)
  • Mock verification for now (will connect to backend later)

Screen 3: UserDetailsScreen

// src/components/screens/UserDetailsScreen.tsx
import React, { useState } from "react";
import {
  View,
  Text,
  TextInput,
  TouchableOpacity,
  StyleSheet,
  ScrollView,
  Alert,
} from "react-native";
import { Picker } from "@react-native-picker/picker";
import { useAuth } from "../../context/AuthContext";
import { AuthNavigationProp } from "../../types/navigation";
import { User } from "../../types/user";

interface Props {
  navigation: AuthNavigationProp;
}

export default function UserDetailsScreen({ navigation }: Props) {
  const { setUser } = useAuth();
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [dateOfBirth, setDateOfBirth] = useState("");
  const [gender, setGender] = useState<"male" | "female" | "other">("male");

  const handleProceed = () => {
    if (!name.trim()) {
      Alert.alert("Validation Error", "Please enter your full name");
      return;
    }

    if (!email.trim() || !email.includes("@")) {
      Alert.alert("Validation Error", "Please enter a valid email address");
      return;
    }

    const user: User = {
      name: name.trim(),
      email: email.trim(),
      mobile: "",
      dateOfBirth: dateOfBirth || undefined,
      gender,
    };

    setUser(user); // This triggers navigation to Main tabs via RootNavigator
  };

  return (
    <ScrollView style={styles.container} contentContainerStyle={styles.content}>
      <Text style={styles.title}>Complete Your Profile</Text>

      {/* Form fields */}
      <View style={styles.inputContainer}>
        <Text style={styles.label}>Full Name *</Text>
        <TextInput
          style={styles.input}
          placeholder="Enter your full name"
          value={name}
          onChangeText={setName}
        />
      </View>

      <View style={styles.inputContainer}>
        <Text style={styles.label}>Email ID *</Text>
        <TextInput
          style={styles.input}
          placeholder="Enter your email"
          keyboardType="email-address"
          autoCapitalize="none"
          value={email}
          onChangeText={setEmail}
        />
      </View>

      <View style={styles.inputContainer}>
        <Text style={styles.label}>Gender</Text>
        <View style={styles.pickerContainer}>
          <Picker
            selectedValue={gender}
            onValueChange={(value) => setGender(value)}
            style={styles.picker}
          >
            <Picker.Item label="Male" value="male" />
            <Picker.Item label="Female" value="female" />
            <Picker.Item label="Other" value="other" />
          </Picker>
        </View>
      </View>

      <TouchableOpacity style={styles.button} onPress={handleProceed}>
        <Text style={styles.buttonText}>Proceed to Home</Text>
      </TouchableOpacity>
    </ScrollView>
  );
}
Enter fullscreen mode Exit fullscreen mode

Key Points:

  • ScrollView for long forms (like mobile web)
  • Picker component for dropdowns (mobile-specific)
  • setUser() triggers navigation automatically (via RootNavigator checking isLoggedIn)

The Complete Flow

Here's how everything connects:

1. App.tsx
   └── AuthProvider (Context)
       └── NavigationContainer
           └── RootNavigator
               ├── If NOT logged in → AuthStackNavigator
               │   ├── LoginScreen
               │   ├── OTPScreen
               │   └── UserDetailsScreen (sets user in context)
               └── If logged in → MainTabNavigator
                   ├── HomeTab
                   ├── TimelineTab
                   ├── SupportTab
                   └── ProfileTab
Enter fullscreen mode Exit fullscreen mode

The Magic:

When UserDetailsScreen calls setUser(user), the AuthContext updates, isLoggedIn becomes true, and RootNavigator automatically switches from AuthStackNavigator to MainTabNavigator. No manual navigation needed!


What I Learned: Key Takeaways

  1. Navigation is More Structured: React Navigation requires more setup than React Router, but it's more type-safe and organized.

  2. Styling is JavaScript: No CSS files—everything is StyleSheet objects. Takes getting used to, but it's actually simpler once you learn it.

  3. Context API is Identical: If you know React Context, you know React Native Context. Zero learning curve here.

  4. Forms Need Mobile Considerations: Use keyboardType for better UX, Alert.alert() for native alerts, and ScrollView for long forms.

  5. TypeScript Types are Essential: Navigation types feel like extra work but prevent bugs and improve autocomplete.

  6. Mobile-Specific Components: TextInput, TouchableOpacity, Picker, ScrollView—these replace HTML elements.


Common Questions (If You're Coming from React)

Q: Can I use CSS in React Native?

A: No, but you can use libraries like styled-components or react-native-css if you really want CSS-like syntax. Most developers use StyleSheet.

Q: How do I handle form validation?

A: Same as React—use state and validation functions. You can also use libraries like react-hook-form (with React Native adapter) or formik.

Q: Can I use React Router in React Native?

A: No, React Router is web-only. React Navigation is the standard for React Native.

Q: How do I debug navigation?

A: Use React Navigation DevTools or console.log the navigation object. The navigation prop has methods like navigate(), goBack(), reset(), etc.

Q: What about deep linking?

A: React Navigation supports deep linking! You configure it in your navigator options.

Q: Can I use the same validation libraries?

A: Yes! Libraries like yup or zod work the same. Form libraries might need React Native adapters.


Problems I Faced & How I Solved Them

Problem 1: TypeScript Navigation Types

Issue: TypeScript errors when navigating between screens.

Solution: Created proper type definitions in src/types/navigation.ts and used them consistently.

Problem 2: Passing Params Between Screens

Issue: Forgot to define params in navigation types.

Solution: Always update AuthStackParamList when adding new screens or params.

Problem 3: Styling Not Working

Issue: Used CSS properties like display: flex instead of React Native properties.

Solution: Remember: React Native uses camelCase and no units. Use flexDirection: 'column' not display: flex.

Problem 4: Keyboard Covering Inputs

Issue: On mobile, keyboard covers input fields.

Solution: Use ScrollView with keyboardShouldPersistTaps="handled" (will cover in future phases).


Resources That Helped Me


Code Repository

All the code from Phase 2 is available on GitHub:


Final Thoughts

Phase 2 was challenging but rewarding. The biggest learning curve was navigation and styling, but once I understood the patterns, everything clicked.

The authentication flow is now complete, and I can see the app structure taking shape. Next up: building the Home screen with goals, progress charts, and appointment booking!

If you're a React developer, navigation and styling are the main differences. Everything else (state, hooks, context) works exactly the same.

Top comments (0)