Welcome to Part 3 of Navigation Re-imagined: Mastering Expo Router, Developer đź‘‹
If you haven’t read it yet, check out Part 2 here to learn about stacks, modals, and route groups.
If you missed the previous parts, we’ve already covered:
- âś… Tab and stack navigation setup
- âś… Dynamic routes, modals, and route groups
In this final part, we’ll learn how to:
- Build an authentication flow using React Context and route protection
- Add deep linking to launch the app into specific screens
- Apply theming for a consistent UI across the app
Let’s keep leveling up your navigation skills!
Table of Contents
Open Table of Contents
Setting Up the Project
Make sure you have the Student Mate repo cloned and checked out to the latest branch:
cd student-mate
git checkout -b post/navigation-expo-router-part-2
npm install
We’ll build everything step-by-step, and you can test each change using npm run start
.
Authentication with React Context
We’ll use React Context to manage the user session and control access to certain screens.
Step 1: Create the Auth Context
This file will define our login state and two methods: one to log in and another to log out.
mkdir -p app/context
Now create app/context/AuthContext.tsx
:
import { createContext, useContext, useState, ReactNode } from "react";
// This defines the shape of our context
interface AuthContextType {
user: string | null;
signIn: (username: string) => void;
signOut: () => void;
}
// We create a Context with this type (initially undefined)
const AuthContext = createContext<AuthContextType | undefined>(undefined);
// The AuthProvider makes user data available throughout the app
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [user, setUser] = useState<string | null>(null);
const signIn = (username: string) => setUser(username); // sets the logged-in user
const signOut = () => setUser(null); // clears the user state
return (
<AuthContext.Provider value={{ user, signIn, signOut }}>
{children}
</AuthContext.Provider>
);
};
// This hook gives components access to the context
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) throw new Error("useAuth must be used within AuthProvider");
return context;
};
Step 2: Wrap the App with AuthProvider
In app/_layout.tsx
, wrap everything inside <AuthProvider>
:
import {
DarkTheme,
DefaultTheme,
ThemeProvider,
} from "@react-navigation/native";
import { useFonts } from "expo-font";
import { Stack } from "expo-router";
import * as SplashScreen from "expo-splash-screen";
import { StatusBar } from "expo-status-bar";
import { useEffect } from "react";
import "react-native-reanimated";
import { useColorScheme } from "@/hooks/useColorScheme";
import { AuthProvider } from "./context/AuthContext";
// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const colorScheme = useColorScheme();
const [loaded] = useFonts({
SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
});
useEffect(() => {
if (loaded) {
SplashScreen.hideAsync();
}
}, [loaded]);
if (!loaded) {
return null;
}
return (
<AuthProvider>
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="+not-found" />
</Stack>
<StatusBar style="auto" />
</ThemeProvider>
</AuthProvider>
);
}
Here’s a quick explanation of what this file does:
- It creates a context (AuthContext) to hold the user state.
- It provides two functions: signIn() and signOut().
- It wraps your app with AuthProvider so every screen can access the auth data.
- It includes a helper hook useAuth() to read the context easily in any screen.
✅ Test: Run the app and confirm it still starts. There’s no visible change yet, but the context is active.
Step 3: Protect Routes
We’ll now restrict the Profile screen so only logged-in users can see it.
Update app/(tabs)/profile.tsx
:
import { Text, View } from "react-native";
import { useAuth } from "../context/AuthContext";
export default function Profile() {
const { user } = useAuth();
if (!user) {
return (
<View>
<Text>You must sign in to access this page.</Text>
</View>
);
}
return (
<View>
<Text>Welcome, {user}</Text>
</View>
);
}
âś… Test: Start the App and navigate to the Profile
screen. It must show the “You must sign in to access this page” message.
Let’s simulate the sign-in flow by adding a text value to our auth context when we click on “Go to Home” on the Login page:
import { router } from "expo-router";
import { Button, StyleSheet, Text, View } from "react-native";
import { useAuth } from "../context/AuthContext";
export default function Login() {
const { signIn } = useAuth();
return (
<View style={styles.container}>
<Text style={styles.title}>đź”’ Login to Student Mate</Text>
<Button
title="Go to Home"
onPress={() => {
signIn("test_user");
router.push("/home");
}}
/>
<Button
title="Go to Register"
onPress={() => {
router.push("/register");
}}
/>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: "center", alignItems: "center" },
title: { fontSize: 24, marginBottom: 20 },
});
✅ Test: Start the App, click on “Go to Home” on Login screen, and navigate to the Profile
screen. It must show the welcome message.
Deep Linking
With Expo Router, deep linking is automatic and works with:
- Custom URL schemes (studentmate://)
- Universal Links / App Links (on iOS and Android)
- Web URLs when running on the browser
Why deep linking matters:
- Improves UX by jumping directly into the relevant screen.
- Supports push notifications, sharing, and external redirects.
- Essential for auth flows, onboarding, and recovery links.
How it works in Expo Router
To make a screen respond to a deep link, just match the URL structure to your file system:
app/
├── assignments/
│ └── [id].tsx # matches studentmate://assignments/123
Step 1: Add scheme to app.json
{
"expo": {
"scheme": "studentmate",
"deepLinks": ["studentmate://assignments"]
}
}
Step 2: Test It
npx uri-scheme open studentmate://assignments/123 --ios
# or for Android
npx uri-scheme open studentmate://assignments/123 --android
It only works if you are running a develop build, which it is not our case. We’re running the app on Expo Go. Therefore, there are a few workarounds we need to do to test it:
- When you run the app with
npm run start
, below the QR code, check the URL onMetro waiting on exp://{server}:{port}
. - Then, run:
npx uri-scheme open exp://{server}:{port}/--/assignments/123 --ios
# or for Android
npx uri-scheme open exp://{server}:{port}/--/assignments/123 --ios
âś… Expected: The app should open directly on the Assignments screen.
Theming
Let’s bring consistency and polish to our UI with React Native Paper, a library of beautiful, Material Design–inspired components that work out of the box with Expo.
Step 1: Install the library
npx expo install react-native-paper
Step 2: Define and Provide a Custom Theme
React Native Paper uses a theme object to control your app’s colors, fonts, and more. Let’s define a custom primary color and apply it globally.
Update app/_layout.tsx
:
import { useFonts } from "expo-font";
import { Stack } from "expo-router";
import * as SplashScreen from "expo-splash-screen";
import { StatusBar } from "expo-status-bar";
import { useEffect } from "react";
import { DefaultTheme, Provider as PaperProvider } from "react-native-paper";
import "react-native-reanimated";
import { useColorScheme } from "@/hooks/useColorScheme";
import { AuthProvider } from "./context/AuthContext";
// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const colorScheme = useColorScheme();
const [loaded] = useFonts({
SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
});
useEffect(() => {
if (loaded) {
SplashScreen.hideAsync();
}
}, [loaded]);
if (!loaded) {
return null;
}
const lightTheme = {
...DefaultTheme,
colors: {
...DefaultTheme.colors,
// copy values from https://callstack.github.io/react-native-paper/docs/guides/theming
},
};
const darkTheme = {
...DefaultTheme,
colors: {
...DefaultTheme.colors,
// copy values from https://callstack.github.io/react-native-paper/docs/guides/theming
},
};
return (
<AuthProvider>
<PaperProvider theme={colorScheme === "dark" ? darkTheme : lightTheme}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="+not-found" />
</Stack>
<StatusBar style="auto" />
</PaperProvider>
</AuthProvider>
);
}
We have added the import to the DefaultTheme and Provider from React Native Paper, created a theme, and wrapped the App with the theme provider.
âś… Test: Start your app and confirm it still loads with no errors.
Step 3: Use Paper Components with the Theme
Let’s update the Home screen to use Paper’s components and styling.
Update app/(tabs)/home.tsx
:
import { Link } from "expo-router";
import { StyleSheet, Text, View } from "react-native";
import { useTheme } from "react-native-paper";
export default function HomeScreen() {
const { colors } = useTheme();
return (
<View style={(styles.container, { backgroundColor: colors.background })}>
<Text style={[styles.title, { color: colors.primary }]}>
Welcome to Student Mate 🎓
</Text>
<Text style={[styles.title, { color: colors.secondary }]}>
Your university companion for managing classes and assignments.
</Text>
<Link href="/classes" style={styles.link}>
View Your Classes
</Link>
<Link href="/assignments" style={styles.link}>
View Assignments
</Link>
<Link href="/profile" style={styles.link}>
Go to Profile
</Link>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
title: { fontSize: 24, marginBottom: 20 },
link: { marginTop: 10, color: "blue" },
});
I’m using the Home as an example just for you to understand how theming works, but the ideal is to use the theme throughout the app.
âś… Test: Navigate to Home page and you should notice the style based on theme we defined. Try to switch between light and dark themes on your simulator to notice how handy it could be, Developer (shift + command + A).
đź§ What We Learned in Part 3
- Authentication with Context: manage sessions and restrict access
- Deep Linking: launch specific screens using URLs or other apps
- Theming: build a visually cohesive UI with shared styles
🎉 Congratulations — You Did It!
You’ve just completed the final part of our Expo Router journey! Together, we built:
- A complete tab + stack + modal navigation system
- A real-world structure with dynamic and protected routes
- Seamless deep linking support
- Theming and reusable UI components
Thanks for following along. Keep building great experiences, and happy coding, Developer đź’™