In this blog post, we will be covering “how to create authenticated routes” with the new Expo SDK 51. This guide is meant to be straightforward for our fellow engineers and it is recommended that you follow along with either an existing expo project or create a new project.
We will skip the introduction of how to set up an expo project and jump straight to explaining what you need to do in order to create simple, yet powerful authenticated routes within your application. As a starter, make sure that your expo project version is "expo": "~51.0.28"
.
Once you have the correct expo version, your folder should look like the image below. The image below is of an internal company project I am working on, so don’t be thrown off by all the other folders but if you were to look at your expo folder structure and the image below, you’ll notice some similarities such as the “app” folder and the “providers” folder. I am not sure if the “providers” folder is created upon creating a new expo project; if not, then please create a “providers” folder.
We are mainly concerned with two folders in our expo project, the “app” folder and the “providers” → “Auth” folder. Ignore all the other folders in the above-posted image unless I specifically mention it in here. These two folders will allow us to create authenticated routes.
Our “app” folder contains “index.tsx” and “_layout.tsx”.
Our “index.tsx” file looks like the image above. When users open up the app, the first thing I present to them is the “onboarding” component. You can change that to whatever you want your users to see first when they first open up the app and they are not logged in. Whether it is the “login” screen or the “sign up” screen, you make that decision.
The main file we are concerned with when creating authenticated routes is the _layout.tsx file. Make sure you copy and paste the contents of this file exactly as it is and I will tell you what you can change. The explanation of the useEffect hook which handles the authentication routing is below.
-
useEffect
Hook on line 38:- The
useEffect
hook is used here to run the logic inside when any of the dependencies (isAuthenticated
,segments
,session
, orisConnected
) change. It ensures that the appropriate routing and authentication logic is executed based on the current state of the user.
- The
-
const authState = segments[0] === "(auth)";
on line 39:- The line
const authState = segments[0] === "(auth)";
checks if the current route starts with"(auth)"
. This indicates that the user is in the authentication part of the app (which includes OnboardingFlow, help, etc.). This checks whether the user is currently trying to access the"(auth)"
part in the app.
- The line
-
Handling Authentication:
- If the user is not authenticated (
!isAuthenticated
) and is currently trying to access an "auth" screen (likeOnboardingFlow
), the user is redirected to the login page.
- If the user is not authenticated (
if (!isAuthenticated && authState) {
return router.replace("/login");
}
-
On line 48, if the user is authenticated and not in the “auth” group (meaning they’re in the main part of the app either login screen or signup screen), the code checks if onboarding is completed.
- If the user hasn’t completed onboarding (
onboardingCompleted
isfalse
), they are redirected to the first onboarding screen"/(auth)/OnboardingFlow/screenOne"
. - If onboarding is completed, they are redirected to the home screen (
"/(tabs)/home"
).
- If the user hasn’t completed onboarding (
else if (isAuthenticated == true && !authState) {
if (onboardingCompleted) {
return router.replace("/(auth)/OnboardingFlow/screenOne");
} else {
return router.replace("/(tabs)/home");
}
}
This means that you can replace the following line to route to any authenticated pages you want
router.replace("/(auth)/OnboardingFlow/screenOne");
or you can simply just immediately route to the home page and avoid the “onboardingCompleted” check which I do in code.
Providers → Auth folder → AuthProviders.tsx
Here is an image of the AuthProvider.tsx
and Auth folder
which we will use to write our sign up
sign in
and sign out
function. In the above _layout.tsx
image you can see that we are wrapping our app with SessionProvider
.
AuthContext and Context Provider Setup
Context is used to share the authentication state (logged in, logged out, onboarding, etc.) across different components in your app.
const AuthContext = createContext<AuthContextType>({
signIn: async () => {},
signOut: () => {},
signUp: async () => {},
session: null,
isLoading: false,
isAuthenticated: false,
onboardingCompleted: false,
});
This creates the authentication context and its initial values.
Context Provider
export function SessionProvider({ children }: PropsWithChildren) {
const [[isSessionLoading, session], setSession] = useStorageState("session");
const [isLoading, setLoading] = useState(false);
const [onboardingCompleted, setOnboardingCompleted] = useState(false);
- The
SessionProvider
wraps your app, giving access to authentication states likesession
,isLoading
, andonboardingCompleted
.
Authentication Functions: Sign Up, Sign In, Sign Out
- These functions handle the core logic for user sign-up, login, and logout processes.
Sign Up Function:
- Firebase’s
auth().createUserWithEmailAndPassword()
creates the user account. - After user creation, you obtain a token (
user.user.getIdTokenResult()
), which can be passed to your backend API to create a corresponding user account in your database. - Errors are handled and appropriate messages are shown to the user.
const signUp = async (email: string, password: string) => {
if (!email && !password) {
showToast("warning", "Missing email and/or password");
setLoading(false);
return;
}
try {
setLoading(true);
const user = await auth().createUserWithEmailAndPassword(email, password);
const token = await user.user.getIdTokenResult();
const data = { email: email, uid: user.user.uid };
const endpoint = "signup";
const response = await actionProvider.makeRequest(endpoint, "POST", data, token.token);
if (response?.success) {
showToast("success", "Welcome! User created successfully.");
setSession(token.token);
} else {
showToast("error", response?.message || "Error creating user.");
}
} catch (error) {
handleSignUpError(error); // e.g., "auth/email-already-in-use"
} finally {
setLoading(false);
}
};
Sign In Function:
- Uses Firebase’s
auth().signInWithEmailAndPassword()
to sign in the user. - The session token is retrieved, and the user’s state is set accordingly.
const signIn = async (email: string, password: string) => {
if (!email && !password) {
showToast("warning", "Missing email and/or password");
setLoading(false);
return;
}
setLoading(true);
try {
const response = await axios.get("http://localhost:5000/api/v1/user");
setOnboardingCompleted(!!response.data.data.completedQuestion);
if (response.data.success) {
setSession("your-session-token");
showToast("success", "Successfully signed in!");
}
} catch (error) {
console.log(error);
showToast("error", "Failed to sign in");
} finally {
setLoading(false);
}
};
Sign Out Function:
- Clears the session by setting
setSession(null)
. - Optionally calls
auth().signOut()
to remove Firebase authentication.
const signOut = async () => {
setLoading(true);
try {
await auth().signOut();
setSession(null);
showToast("success", "Signed out successfully!");
} catch (error) {
showToast("error", "Failed to sign out");
} finally {
setLoading(false);
}
};
Exposed Values in Context:
- This is where the power of context lies: all the essential authentication state and functions are passed through the
value
prop, making them available throughout the app.
return (
<AuthContext.Provider
value={{
signIn,
signOut,
signUp,
session,
isLoading: isLoading || isSessionLoading,
isAuthenticated,
onboardingCompleted,
}}
>
{children}
</AuthContext.Provider>
);
Providers → Auth folder → useStorageState.tsx
useStorageState
is a custom hook designed to manage storage in mobile. It helps persist the user’s session across app restarts, which is critical for keeping users logged in after closing the app.
When a user signs in, their session token (or authentication token) is generated. This token needs to be stored securely and retrieved later when the app is reopened. useStorageState
handles this.
Here is the code. You can remove the “web” checking part.
import * as SecureStore from "expo-secure-store";
import * as React from "react";
import { Platform } from "react-native";
type UseStateHook<T> = [[boolean, T | null], (value: T | null) => void];
function useAsyncState<T>(
initialValue: [boolean, T | null] = [true, null]
): UseStateHook<T> {
return React.useReducer(
(
state: [boolean, T | null],
action: T | null = null
): [boolean, T | null] => [false, action],
initialValue
) as UseStateHook<T>;
}
export async function setStorageItemAsync(key: string, value: string | null) {
if (Platform.OS === "web") {
try {
if (value === null) {
localStorage.removeItem(key);
} else {
localStorage.setItem(key, value);
}
} catch (e) {
console.error("Local storage is unavailable:", e);
}
} else {
if (value == null) {
await SecureStore.deleteItemAsync(key);
} else {
await SecureStore.setItemAsync(key, value);
}
}
}
export function useStorageState(key: any): UseStateHook<string> {
const [state, setState] = useAsyncState<string>();
React.useEffect(() => {
if (Platform.OS === "web") {
try {
if (typeof localStorage !== "undefined") {
setState(localStorage.getItem(key));
}
} catch (e) {
console.error("Local storage is unavailable:", e);
}
} else {
SecureStore.getItemAsync(key).then((value) => {
setState(value);
});
}
}, [key]);
const setValue = React.useCallback(
(value: string | null) => {
setState(value);
setStorageItemAsync(key, value);
},
[key]
);
return [state, setValue];
}
useStorageState
leverages Expo SecureStore to store and retrieve sensitive data, such as a user’s session token, securely. Secure storage is critical on mobile to protect user data from unauthorized access, especially when dealing with authentication tokens.
Conclusion
By using SessionProvider
, expo-router
, and hooks like useSegments
, we've built a flexible routing system that ensures only authenticated users can access protected areas of our app. The key is using context to share authentication state and routing logic that redirects users to the correct screens based on their authentication and onboarding status.
This setup provides a robust way to manage routes and authentication in Expo, ensuring a secure and user-friendly flow for mobile apps.
If you need any help → email me at hello@detl.ca or simply connect with me on LinkedIn.
Top comments (0)