This blog post is a complimentary resource to support the video on Expo Router v2
In the video, I walk you through the code for an authentication flow using Expo Router and Appwrite.
I explain how to set up the environment variables, install the router, and incorporate Appwrite as the authentication provider. I also demonstrate how to create login and signup pages, manage authentication state, and handle navigation.
The video provides a step-by-step guide with code examples and explanations. Check it out to learn how to build a secure authentication flow for your Expo app!
Create your Appwrite Project
Appwrite is a backend platform for developing Web, Mobile, and Flutter applications. Built with the open source community and optimized for developer experience in the coding languages you love.
This command will create the default app with the latest version of expo sdk and expo router. We will be adding an authentication flow to the application and integrating Appwrite Account Creation and Login.
Add Appwrite Library To Project
This code will set up the Appwrite client and provides a convenient interface to interact with the Appwrite server in a react native application.
First you need to create the .env file to hold the Appwrite Project Id
1) Importing the required dependencies
2) Creating a new Appwrite client instance and Configuring the client. This is where we using the value from the .env file
3) Exporting the Appwrite client and related objects; we need the account object for the authentication and account creation functionality. The ID object generates unique identifiers that are utilized when creating the new user account
Add Auth ProviderBased On Expo Router Documentation
I won't spend a lot of time explaining what the Provider is and how React Context works, you can click here for additional information on React Context API
I will cover the changes made to support Typescript and the integration of the appwrite-service library.
This code sets up an authentication context and provider in a React application, provides authentication-related functions, and allows other components to consume the authentication context using the useAuth hook. It handles user authentication, route protection, and authentication-related API interactions with the Appwrite service.
// /app/context/auth.ts// ---- (1) ----import{useRootNavigation,useRouter,useSegments}from"expo-router";importReact,{useContext,useEffect,useState}from"react";import{appwrite}from"../lib/appwrite-service";import{Models}from"appwrite";// ---- (2) ----// Define the AuthContextValue interfaceinterfaceSignInResponse{data:Models.User<Models.Preferences>|undefined;error:Error|undefined;}interfaceSignOutResponse{error:any|undefined;data:{}|undefined;}interfaceAuthContextValue{signIn:(e:string,p:string)=>Promise<SignInResponse>;signUp:(e:string,p:string,n:string)=>Promise<SignInResponse>;signOut:()=>Promise<SignOutResponse>;user:Models.User<Models.Preferences>|null;authInitialized:boolean;}// Define the Provider componentinterfaceProviderProps{children:React.ReactNode;}// ---- (3) ----// Create the AuthContextconstAuthContext=React.createContext<AuthContextValue|undefined>(undefined);// ---- (4) ----exportfunctionProvider(props:ProviderProps){// ---- (5) ----const[user,setAuth]=React.useState<Models.User<Models.Preferences>|null>(null);const[authInitialized,setAuthInitialized]=React.useState<boolean>(false);// This hook will protect the route access based on user authentication.// ---- (6) ----constuseProtectedRoute=(user:Models.User<Models.Preferences>|null)=>{constsegments=useSegments();constrouter=useRouter();// checking that navigation is all good;// ---- (7) ----const[isNavigationReady,setNavigationReady]=useState(false);constrootNavigation=useRootNavigation();// ---- (8) ----useEffect(()=>{constunsubscribe=rootNavigation?.addListener("state",(event)=>{setNavigationReady(true);});returnfunctioncleanup(){if (unsubscribe){unsubscribe();}};},[rootNavigation]);// ---- (9) ----React.useEffect(()=>{if (!isNavigationReady){return;}constinAuthGroup=segments[0]==="(auth)";if (!authInitialized)return;if (// If the user is not signed in and the initial segment is not anything in the auth group.!user&&!inAuthGroup){// Redirect to the sign-in page.router.push("/sign-in");}elseif (user&&inAuthGroup){// Redirect away from the sign-in page.router.push("/");}},[user,segments,authInitialized,isNavigationReady]);};// ---- (10) ----useEffect(()=>{(async ()=>{try{constuser=awaitappwrite.account.get();console.log(user);setAuth(user);}catch (error){console.log("error",error);setAuth(null);}setAuthInitialized(true);console.log("initialize ",user);})();},[]);/**
*
* @returns
*/// ---- (11) ----constlogout=async ():Promise<SignOutResponse>=>{try{constresponse=awaitappwrite.account.deleteSession("current");return{error:undefined,data:response};}catch (error){return{error,data:undefined};}finally{setAuth(null);}};/**
*
* @param email
* @param password
* @returns
*/// ---- (12) ----constlogin=async (email:string,password:string):Promise<SignInResponse>=>{try{console.log(email,password);constresponse=awaitappwrite.account.createEmailSession(email,password);constuser=awaitappwrite.account.get();setAuth(user);return{data:user,error:undefined};}catch (error){setAuth(null);return{error:errorasError,data:undefined};}};/**
*
* @param email
* @param password
* @param username
* @returns
*/// ---- (13) ----constcreateAcount=async (email:string,password:string,username:string):Promise<SignInResponse>=>{try{console.log(email,password,username);// create the userawaitappwrite.account.create(appwrite.ID.unique(),email,password,username);// create the session by logging inawaitappwrite.account.createEmailSession(email,password);// get Account information for the userconstuser=awaitappwrite.account.get();setAuth(user);return{data:user,error:undefined};}catch (error){setAuth(null);return{error:errorasError,data:undefined};}};useProtectedRoute(user);// ---- (14) ----return (<AuthContext.Providervalue={{signIn:login,signOut:logout,signUp:createAcount,user,authInitialized,}}>{props.children}</AuthContext.Provider>);}// Define the useAuth hook// ---- (15) ----exportconstuseAuth=()=>{constauthContext=useContext(AuthContext);if (!authContext){thrownewError("useAuth must be used within an AuthContextProvider");}returnauthContext;};
1) Imports
2) Interfaces for defining the shape of the data returned by the context and responses from the API calls that are utilized in the provider and exposed to the application. The api calls are all setup to return data and error.
3) The AuthContext is created using React.createContext, and the initial value is set to undefined. This context will hold the authentication-related functions and values to be shared with other components.
4)The Provider component is exported. It serves as the provider for the authentication context and wraps its children with the AuthContext.Provider component.
5) Set state variables for the Provider, we keep track of the user with user and we set the variable using setAuth. We use authInitialized to let us know when the application has completed it's check for an existing session that hasn't expired
6) useProtectedRouter is hook called to properly redirect the application to the sign-in route if there is no valid session.
7) local state variables for the hook. isNavigationReady is a check to make sure the navigation is all set up before we attempt to route anywhere in the application
8) useEffect to set up the listener for the rootNavigation state
9) useEffect to route user to proper app location based on what segment the route is in and what the user state variable is set to. This is not called unless authInitialized and isNavigationReady
10) useEffect call when the component is mounted to see if there is a valid user session, We use the appwrite API appwrite.account.get() and set user state with setAuth
11) use Appwrite API call appwrite.account.deleteSession to log the user out
12) use Appwrite API call appwrite.account. createEmailSession to log the user out
13) createAccount is a bit more detailed, you need to first create the user account using appwrite.account.create(), the log the user in using the appwrite.account.createEmailSession api call and then finally get the user information with appwrite.account.get() and set user state with setAuth
14) The Provider component returns the AuthContext.Provider component, which wraps the props.children. It provides the authentication-related values and functions as the context value.
15) The useAuth hook is exported, which allows other components to access the authentication context and retrieve the authentication-related values and functions.
Controlling Navigation Stack In _layout.tsx
This code represents the root layout of a mobile application using React Native and Expo. It sets up the navigation, themes, fonts, and authentication context for the application.
In the code below, the important part is how the code renders the application's content wrapped in the authentication Provider. We need to wrap it in the Provider so we have access to the useAuth hook in the RootLayoutNav
// /app/_layout.tsxexportdefaultfunctionRootLayout(){const[loaded,error]=useFonts({SpaceMono:require("../assets/fonts/SpaceMono-Regular.ttf"),...FontAwesome.font,});// Expo Router uses Error Boundaries to catch errors in the navigation tree.useEffect(()=>{if (error)throwerror;},[error]);useEffect(()=>{if (loaded){SplashScreen.hideAsync();}},[loaded]);if (!loaded){returnnull;}return (<Provider><RootLayoutNav/></Provider>);}
In the code below we use the useAuth hook to check if the authentication has been initialized and if a user is authenticated. If the authentication is not initialized or there is no user, it returns null to render nothing. Otherwise, it renders the application's content wrapped in the ThemeProvider and sets up two screens in a Stack navigator: "(tabs)" and "modal".
The folder named (auth) is where we place the sign-in and sign-up screens. Since we are using file based routing we just need to place the files in the folder and expo router does the rest for us.
Sign In Page
// /app/(auth)/sign-inimport{Text,TextInput,View,StyleSheet,TouchableOpacity,}from"react-native";import{useAuth}from"../context/auth";import{Stack,useRouter}from"expo-router";import{useRef}from"react";exportdefaultfunctionSignIn(){const{signIn}=useAuth();constrouter=useRouter();constemailRef=useRef("");constpasswordRef=useRef("");return (<><Stack.Screenoptions={{title:"sign up",headerShown:false}}/><Viewstyle={{flex:1,justifyContent:"center",alignItems:"center"}}><View><Textstyle={styles.label}>Email</Text><TextInputplaceholder="email"autoCapitalize="none"nativeID="email"onChangeText={(text)=>{emailRef.current=text;}}style={styles.textInput}/></View><View><Textstyle={styles.label}>Password</Text><TextInputplaceholder="password"secureTextEntry={true}nativeID="password"onChangeText={(text)=>{passwordRef.current=text;}}style={styles.textInput}/></View><TouchableOpacityonPress={async ()=>{const{data,error}=awaitsignIn(emailRef.current,passwordRef.current);if (data){router.replace("/");}else{console.log(error);// Alert.alert("Login Error", resp.error?.message);}}}style={styles.button}><Textstyle={styles.buttonText}>Login</Text></TouchableOpacity><Viewstyle={{marginTop:32}}><Textstyle={{fontWeight:"500"}}onPress={()=>router.push("/sign-up")}>
Click Here To Create A New Account
</Text></View></View></>);}conststyles=StyleSheet.create({label:{marginBottom:4,color:"#455fff",},textInput:{width:250,borderWidth:1,borderRadius:4,borderColor:"#455fff",paddingHorizontal:8,paddingVertical:4,marginBottom:16,},button:{backgroundColor:"blue",padding:10,width:250,borderRadius:5,marginTop:16,},buttonText:{color:"white",textAlign:"center",fontSize:16,},});
Nothing special happening in this file other than importing useAuth so we have access to the signIn function from the AuthContext.
Sign Up Page
// /app/(auth)/sign-upimport{Text,View,StyleSheet,TextInput,TouchableOpacity,}from"react-native";import{useAuth}from"../context/auth";import{Stack,useRouter}from"expo-router";import{useRef}from"react";exportdefaultfunctionSignUp(){const{signUp}=useAuth();constrouter=useRouter();constemailRef=useRef("");constpasswordRef=useRef("");constuserNameRef=useRef("");return (<><Stack.Screenoptions={{title:"sign up",headerShown:false}}/><Viewstyle={{flex:1,justifyContent:"center",alignItems:"center"}}><View><Textstyle={styles.label}>UserName</Text><TextInputplaceholder="Username"autoCapitalize="none"nativeID="userName"onChangeText={(text)=>{userNameRef.current=text;}}style={styles.textInput}/></View><View><Textstyle={styles.label}>Email</Text><TextInputplaceholder="email"autoCapitalize="none"nativeID="email"onChangeText={(text)=>{emailRef.current=text;}}style={styles.textInput}/></View><View><Textstyle={styles.label}>Password</Text><TextInputplaceholder="password"secureTextEntry={true}nativeID="password"onChangeText={(text)=>{passwordRef.current=text;}}style={styles.textInput}/></View><TouchableOpacityonPress={async ()=>{const{data,error}=awaitsignUp(emailRef.current,passwordRef.current,userNameRef.current);if (data){router.replace("/");}else{console.log(error);// Alert.alert("Login Error", resp.error?.message);}}}style={styles.button}><Textstyle={styles.buttonText}>Create Account</Text></TouchableOpacity><Viewstyle={{marginTop:32}}><Textstyle={{fontWeight:"500"}}onPress={()=>router.replace("/sign-in")}>
Click Here To Return To Sign In Page
</Text></View></View></>);}conststyles=StyleSheet.create({label:{marginBottom:4,color:"#455fff",},textInput:{width:250,borderWidth:1,borderRadius:4,borderColor:"#455fff",paddingHorizontal:8,paddingVertical:4,marginBottom:16,},button:{backgroundColor:"blue",padding:10,width:250,borderRadius:5,marginTop:16,},buttonText:{color:"white",textAlign:"center",fontSize:16,},});
Nothing special happening in this file other than importing useAuth so we have access to the signUp function from the AuthContext.
Handling Sign Out
We modified the first Tab Page content to include a logout button. In the page we once again import useAuth to get access to the signOut function.
import{StyleSheet}from'react-native';importEditScreenInfofrom'@/components/EditScreenInfo';import{Text,View}from'@/components/Themed';import{useAuth}from'../context/auth';exportdefaultfunctionTabOneScreen(){const{signOut,user}=useAuth();return (<Viewstyle={styles.container}><Textstyle={styles.title}>Tab One</Text><Viewstyle={styles.separator}lightColor="#eee"darkColor="rgba(255,255,255,0.1)"/><EditScreenInfopath="app/(tabs)/index.tsx"/><TextonPress={()=>signOut()}>Sign Out - {user?.email}</Text></View>);}conststyles=StyleSheet.create({container:{flex:1,alignItems:'center',justifyContent:'center',},title:{fontSize:20,fontWeight:'bold',},separator:{marginVertical:30,height:1,width:'80%',},});
Wrap Up
This pattern can be used with any account management solution. I used Appwrite only becuase it was something different and I like to mix things up a bit. You could have easily integrated Firebase or Supabase into the AuthContext and the application will work perfectly for you.
I hope you found this helpful, please check out the video and the rest of the content on my YouTube Channel.
Expo Router - File Based Routing for React Native, tabs template with auth flow using context api
expo-router-v2-authflow-appwrite
Expo Router - File Based Routing for React Native, tabs template with auth flow using context API
Click Here If You Are Looking For Supabase Source Code - SUPABASE AUTH FLOW
This is the source code from the video on Expo Router v2
In the video, I walk you through the code for an authentication flow using Expo Router and Appwrite.
I explain how to set up the environment variables, install the router, and incorporate Apprite as the authentication provider. I also demonstrate how to create login and signup pages, manage authentication state, and handle navigation.
The video provides a step-by-step guide with code examples and explanations. Check it out to learn how to build a secure authentication flow for your Expo app!