DEV Community

DETL INC
DETL INC

Posted on

How to create authenticated routes with the new Expo SDK 51 using Expo Router

Image description

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.

Image description

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”.

Image description

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.

Image description

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, or isConnected) change. It ensures that the appropriate routing and authentication logic is executed based on the current state of the user.
  • 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.

Image description

  • Handling Authentication:

    • If the user is not authenticated (!isAuthenticated) and is currently trying to access an "auth" screen (like OnboardingFlow), the user is redirected to the login page.
if (!isAuthenticated && authState) {
  return router.replace("/login");
}
Enter fullscreen mode Exit fullscreen mode
  • 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 is false), they are redirected to the first onboarding screen "/(auth)/OnboardingFlow/screenOne".
    • If onboarding is completed, they are redirected to the home screen ("/(tabs)/home").
else if (isAuthenticated == true && !authState) {
  if (onboardingCompleted) {
    return router.replace("/(auth)/OnboardingFlow/screenOne");
  } else {
    return router.replace("/(tabs)/home");
  }
}
Enter fullscreen mode Exit fullscreen mode

This means that you can replace the following line to route to any authenticated pages you want

router.replace("/(auth)/OnboardingFlow/screenOne");

Enter fullscreen mode Exit fullscreen mode

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.

Image description

Image description

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,
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode
  • The SessionProvider wraps your app, giving access to authentication states like session, isLoading, and onboardingCompleted.

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);
  }
};

Enter fullscreen mode Exit fullscreen mode

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);
  }
};
Enter fullscreen mode Exit fullscreen mode

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);
  }
};
Enter fullscreen mode Exit fullscreen mode

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>
  );
Enter fullscreen mode Exit fullscreen mode

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.

Image description

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];
}
Enter fullscreen mode Exit fullscreen mode

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)