Skip to content

Navigation Re-imagined: Mastering Expo Router - Protected Routes, Deep Linking & Theming

Posted on:May 13, 2025

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:

In this final part, we’ll learn how to:

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:

  1. It creates a context (AuthContext) to hold the user state.
  2. It provides two functions: signIn() and signOut().
  3. It wraps your app with AuthProvider so every screen can access the auth data.
  4. 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:

Why deep linking matters:

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:

  1. When you run the app with npm run start, below the QR code, check the URL on Metro waiting on exp://{server}:{port}.
  2. 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


🎉 Congratulations — You Did It!

You’ve just completed the final part of our Expo Router journey! Together, we built:

Thanks for following along. Keep building great experiences, and happy coding, Developer đź’™