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:
- Project folder structure (organizing code like a pro)
- Navigation system (Auth stack + Main tabs)
- OTP-based login flow (Login → OTP → Profile Setup)
- 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
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
Key Differences:
-
screens/folder: In React Native, we think in terms of "screens" (full-screen views) rather than "pages" -
navigation/folder: Navigation is more complex in mobile apps, so it gets its own folder -
types/folder: TypeScript types are crucial for navigation type safety (more on this later) -
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>
);
}
Navigation:
import { useNavigate } from "react-router-dom";
function Login() {
const navigate = useNavigate();
const handleLogin = () => {
navigate("/home");
};
}
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>
);
}
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>
);
}
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" });
};
}
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>;
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!
In React Navigation with TypeScript:
navigation.navigate("OTP", { mobile: "9876543210" }); // TypeScript ensures mobile exists
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);
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;
};
Usage (Same in Both):
// In any component
const { user, setUser, isLoggedIn } = useAuth();
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>
);
}
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>
);
}
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>
);
}
/* 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;
}
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",
},
});
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:
- No CSS Files: Everything is JavaScript objects
-
CamelCase Properties:
background-color→backgroundColor -
No Units: Numbers are in logical pixels (no
px,em,rem) - Flexbox by Default: Every container is a flex container
-
Limited Properties: No
hover,:before,:after, etc. - 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>
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",
},
});
What's Happening:
- User enters mobile number
- On button press, we validate (10 digits)
- If valid, navigate to OTP screen with mobile number as param
- 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>
);
}
Key Points:
-
route.paramsgives you the data passed from previous screen - TypeScript ensures
mobileexists (fromAuthStackParamList) - 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>
);
}
Key Points:
-
ScrollViewfor long forms (like mobile web) -
Pickercomponent for dropdowns (mobile-specific) -
setUser()triggers navigation automatically (viaRootNavigatorcheckingisLoggedIn)
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
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
Navigation is More Structured: React Navigation requires more setup than React Router, but it's more type-safe and organized.
Styling is JavaScript: No CSS files—everything is StyleSheet objects. Takes getting used to, but it's actually simpler once you learn it.
Context API is Identical: If you know React Context, you know React Native Context. Zero learning curve here.
Forms Need Mobile Considerations: Use
keyboardTypefor better UX,Alert.alert()for native alerts, andScrollViewfor long forms.TypeScript Types are Essential: Navigation types feel like extra work but prevent bugs and improve autocomplete.
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
- React Navigation Documentation - Excellent guide with examples
- React Native Styling Guide - Official styling documentation
- TypeScript with React Navigation - Type safety guide
Code Repository
All the code from Phase 2 is available on GitHub:
- physio-care-react-native-first-project - Complete source code
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)