mulungood

Protecting routes with Expo 49

How to implement authentication and authorization logic, and organize your app's pages for an optimal user experience.

Frederico Santiago's photoFrederico Santiago

The Expo docs on authentication served as a starter to understanding how such a thing could be accomplished, but it didn’t work all the way, for it tries to redirect the app to a different route before the Slot component in _layout.jsx finishes mounting. Mainly a problem with the fact that the default route is not accessible for an unauthenticated user, and thus gets redirected to a login page right away.

So, lets make it work with a few simple tweaks. You can clone the repo:

First, we start by defining two folders inside of the app folder: (auth) and (noauth) — the parenthesis here are important so these folders’ names don’t appear at their routes (but you should use them when linking one page to another).

Everything that’s inside of (auth) will only be accessible to an authenticated user. The (noauth) folder isn’t necessary for the logic here, it’s just a way of keeping things organized.

We’ll just have two pages in our app: the main one (index.tsx) will be our sign in page and the authenticatedPage will be the protected one. We’re not implementing any real authentication flow here, so the sign in will be just to click on a sign in button (the same goes for sign out):

So index.tsx looks like this:

import { Text, View, Pressable } from "react-native"; import { Link } from "expo-router"; import { useAuth } from "../../context/authProvider.tsx"; export default function Index() { const { signIn } = useAuth(); return ( <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}> <Pressable> <Text onPress={() => signIn()}>Sign In</Text> </Pressable> </View> ); }

And authenticatedPage.tsx:

import { View, Text } from "react-native"; import { SignOut } from "../../components/signOut.tsx"; export default function AuthenticatedPage() { return ( <View style={{ flex: 1 }}> <View style={{ position: "absolute", top: 80, left: 50 }}> <SignOut /> </View> <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}> <Text>Authenticated page</Text> </View> </View> ); }

Once authenticated, the user has the option to sign out, which is done by the imported SingOut component that looks like this:

import { View, Text, Pressable } from "react-native"; import { useAuth } from "../context/authProvider.tsx"; export function SignOut() { const { signOut } = useAuth(); return ( <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}> <Pressable> <Text onPress={() => signOut()}>Sign Out</Text> </Pressable> </View> ); }

All these are kept extremely simple, so we can focus on the authentication provider logic, which is the interesting part here.

So inside of the authProvider.tsx file, we have some things going on. Let’s have a look at its entire code and then discuss its parts:

import { router, useSegments } from "expo-router"; import { useEffect, useState, useContext, createContext } from "react"; const AuthContext = createContext({ signIn: () => {}, signOut: () => {}, user: {}, }); // This hook can be used to access the user info. export function useAuth() { return useContext(AuthContext); } // This hook will protect the route access based on user authentication. function useProtectedRoute(user: {} | null) { const segments = useSegments(); useEffect(() => { const inAuthGroup = segments[0] === "(auth)"; if ( // If the user is not signed in. !user ) { // Redirect to the sign-in page. if (segments.length !== 0) router.replace("/"); } else if (user && !inAuthGroup) { // Redirect away from the sign-in page to the page the user was trying to access. // From now on, as long as the user stays authenticated and tries to access // pages that are in the (auth) folder, they will be able to access them. router.replace("/(auth)/authenticatedPage"); } }, [user, segments]); } export function AuthProvider(props: any) { const [user, setUser] = useState<{} | null>(null); useProtectedRoute(user); return ( <AuthContext.Provider value={{ signIn: () => setUser({}), signOut: () => setUser(null), user, }} > {props.children} </AuthContext.Provider> ); }

We first define the AuthContext and the react hook useAuth() that exposes it. Inside of AuthProvider we just define the user state variable that will be our parameter to see if the user is authenticated or not. For this mock logic, if user is null, it shouldn’t be granted access to authenticated only pages. On the other hand, if it’s {}, it’s authenticated and should be taken to the authenticatedPage.

Thus, the signIn and signOut functions simply set user to {} and null, respectively.

Now, to the interesting part: the useProtectedRoute.

We use the useSegments hook to find out if the route we’re currently on is based on the (auth) folder. If it is, then segments[0] is "(auth)". The logic is pretty straight-forward here, but can be as sophisticated as needed for each project.

If (!user) means the user is not signed in, so we force them to stay on the sign in page replacing the router path to "/" (which is the path that takes to "(noauth)/index.tsx"). Replacing here is important so we don’t stack any unauthorized routes the user may try to access in the navigation history.

But then, if user is not null, it means that they’re signed in. If the route they’re in is already inside of (auth) folder, there’s no routing to be modified and they’re free to browse the app (if only there were pages for them to see in the app). But if not, then it means that the user is signed in but still in a (noauth) path. In this case, we force the route to "/(auth)/authenticatedPage" with the replace function.

And that’s it.

This is an open learning experience for me. If you have any suggestions or questions, feel free to reach me at frederico@mulungood.com