
Table of Contents
- Introduction
- React Project Setup with Appwrite
- Building the User Interface(UI)
- Set up React Context for User State Management
- User Registration functionality
- User Login functionality
- Private Route
- User Persistence
- Handling User Logout
- Conclusion
Introduction
User Authentication as we know it today has become very important in Web applications. Authentication is simply the process of identifying a users identity. For so long now, things like user Authentication has been handled by custom backend in applications which then sends the data to the frontend and all the magic happens, it's usually time consuming, complex and hard to grasp especially if you're not a backend developer. During the course of this article, we will implement user authentication in React web applications using Appwrite.
Appwrite is a versatile open-source backend server that offers various functionalities like authentication, database, storage, and server-side functions. It enables developers to build full-stack applications without requiring a separate backend. With Appwrite, React developers can create real-time applications effortlessly, leveraging its comprehensive set of APIs and services.
This article will guide you through implementing User Authentication using the AppWrite Auth functionality.
For this article, it's helpful to have a basic understanding of handling forms in React and familiarity with React Router. Don't worry if you're not an expert yet, you can still follow along and learn as you go!
React Project Setup with Appwrite
To initiate our project setup, the first step involves registering and creating an account withAppWrite
After registering, create a new project in appwrite by clicking on theCreate a new project
button. You would then see a User Interface (UI) like this:
To get started, you'll need to give your project a name. We'll usereact-appwrite-auth
for this example. Don't worry about the Project ID, it will be automatically generated for you when you clickNext
On click of theNext
button you'll be routed to a page where you'll be asked to choose your region. Choose the nearest available region for optimal performance.
Next click on theCreate
button, this will redirect you to your project dashboard where you'll be able to Add a platform. Since we're using React, we will go with theWeb
Platform
On Click of theWeb Platform
we are presented with a screen in the screenshot below.
These two fields are required. Set the name asReact Appwrite Auth
and the hostname aslocalhost
. Alternatively, you can use*
as your hostname.
We would then see two optional steps that guide us on how toInstall the appwrite SDK into our app
andInitialize and configure the API Key
. We would come back to use them after we've successfully created our React application locally.
After these we would see the screen in the screenshot below.
Hooray! We have successfully setup our project on the Appwrite Platform.
Now, Let us create a React Project using vite. Run the following commands to create a React project.
Create a project directory namedreact-appwrite-auth
and open it in your favorite code editor. Then run the following command that helps to scaffold(find a more simple word) a project for you.
# npmnpm create vite@latest# Yarnyarn create vite
You will then be prompted and asked to configure how you want your project to be configured. The screenshot below should direct you on what you should pick
Entering
./
as the project name in Vite scaffolds the React project in your current terminal directory. ChooseReact
from the framework options, and select theJavascript
variant.
After creating the project,npm install
fetches the necessary tools andnpm run dev
starts a live development server for quick updates.
Now let us install Appwrite into our created React Project and configure the API keys
To install Appwrite, run the following command in the projects directory
npminstallappwrite
Next, create a.env.local
file in the project root directory and paste the following
VITE_APPWRITE_PROJECT_ID="PROJECT_ID"
To obtain yourPROJECT_ID
, navigate to your project created on appwrite, in our case, navigate to thereact-appwrite-auth
project, you will be able to see it here.
You can also navigate to thereact-appwrite-auth
project dashboard >settings
, you will be able to see your PROJECT_ID similar to the screenshot below
Then create aappwriteConfig.js
file in thesrc
folder of your React app and paste the following code
import{Account,Client}from"appwrite";// Import Client from "appwrite"constclient=newClient();// Create a new Client instanceclient.setEndpoint("https://cloud.appwrite.io/v1").setProject(import.meta.env.VITE_APPWRITE_PROJECT_ID)exportconstaccount=newAccount(client);// Export an Account instance initialized with the client
We have successfully created the Appwrite client by passing in the Project EndPoint and Project ID and then exporting the account instance that will be used later on in the project.
Building the User Interface(UI)
This section focuses on crafting UI components for our React app, prioritizing simplicity and functionality due to our authentication focus. We'll build four key components:
- Login Page: Initial access point for users to input credentials and log in
- Register Page: Allows new users to create accounts by providing essential details.
- Home Page (Private Route): Authenticated users' exclusive space, displaying user email and ID from Appwrite.
- The Navbar Component
Login Page
import{useState}from"react";import{Link}from"react-router-dom";import"./Login.css";constLogin=()=>{const[email,setEmail]=useState("");const[password,setPassword]=useState("");const[buttonLoading,setButtonLoading]=useState(false);consthandleLogin=async(e)=>{setButtonLoading(true)e.preventDefault();if(password===""||email===""){alert("Please fill in the field required")setButtonLoading(false)return}// appwrite Login functionality 👇return(<divclassName="loginPage"><h2>Login</h2><formonSubmit={handleLogin}><div><labelhtmlFor="email">Email:</label><inputtype="email"requiredid="email"value={email}onChange={(e)=>setEmail(e.target.value)}/></div><div><labelhtmlFor="password">Password:</label><inputtype="password"requiredid="password"value={password}onChange={(e)=>setPassword(e.target.value)}/></div><buttontype="submit">{buttonLoading?"Loading...":"Login"}</button><div>Donthaveanaccount?<Linkto="/register">Register</Link></div></form></div>);};exportdefaultLogin;
.loginPage>form>*:not(:last-child){margin-bottom:1rem;}
Register Page
import{useState}from"react";import{Link}from"react-router-dom";import"./Register.css";constRegister=()=>{const[username,setUsername]=useState("");const[email,setEmail]=useState("");const[password,setPassword]=useState("");const[confirmPassword,setConfirmPassword]=useState("");const[loadingStatus,setLoadingStatus]=useState(false);consthandleRegister=async(e)=>{e.preventDefault();try{// Call Appwrite function to handle user registrationif(password!==confirmPassword){alert("Passwords do not match");setLoadingStatus(false)return;}if(username===""||email===""||password===""||confirmPassword===""){alert("Please fill in all fields");setLoadingStatus(false)return;}// appwrite Register functionality 👇}catch(err){alert(err.message);}};return(<divclassName="registerPage"><h2>Register</h2><formonSubmit={handleRegister}><div><labelhtmlFor="username">Username:</label><inputtype="text"requiredid="username"value={username}onChange={(e)=>setUsername(e.target.value)}/></div><div><labelhtmlFor="email">Email:</label><inputtype="email"requiredid="email"value={email}onChange={(e)=>setEmail(e.target.value)}/></div><div><labelhtmlFor="password">Password:</label><inputtype="password"requiredid="password"value={password}onChange={(e)=>setPassword(e.target.value)}/></div><div><labelhtmlFor="confirmPassword">ConfirmPassword:</label><inputtype="password"requiredid="confirmPassword"value={confirmPassword}onChange={(e)=>setConfirmPassword(e.target.value)}/></div><buttontype="submit">{<buttontype="submit">{loadingStatus?"Loading...":"Register"}</button>}</button><div>Haveanaccount?<Linkto="/login">Login</Link></div></form></div>);};exportdefaultRegister;
.registerPage>form>*:not(last-child){margin-bottom:1rem;}
Home Page
import"./Home.css";constHome=()=>{return(<div><h1>HomePage</h1><p>ThispageisaProtectedPageandshouldonlybeseenbyAuthenticatedUsers</p></div>);};exportdefaultHome;
Navbar Component
import{Link}from"react-router-dom";import"./Navbar.css";import{useNavigate}from"react-router-dom";constNavbar=()=>{constnavigate=useNavigate();consthandleLogin=()=>{navigate("/login")}return(<nav><divclassName="navLogo">Logo</div><Linkto="/"className="navHomeLink">Home</Link><buttononClick={handleLogin}className="navLoginButton">Login</button></nav>);};exportdefaultNavbar;
nav{/* max-width: 768px; */margin-inline:auto;display:flex;align-items:center;justify-content:space-between;border-bottom:3pxsolidblack;}nav>.navLogo{font-size:2.5rem;}nav>.navHomeLink{font-size:1.4rem;}nav>button{background-color:transparent;border:1pxsolidgray;font-size:1.2rem;cursor:pointer;}
If you've followed the UI code snippets, you'll have a basic app that looks like this:
Set up React Context for User State Management
In your root directory, create acontext
folder. Inside it, add a file namedUserAuthContext.jsx
. Paste the provided code into this file to handle user authentication efficiently.
import{createContext,useEffect,useState}from"react";import{account}from"../appwriteConfig";exportconstUserAuthContext=createContext();exportconstUserProvider=({children})=>{const[user,setUser]=useState(null);const[isLoading,setIsLoading]=useState(true);// Track loading stateuseEffect(()=>{constfetchUserData=async()=>{try{constresponse=awaitaccount.get();// Fetch user datasetUser(response);// Set user data}catch(error){console.error("Error fetching user data:",error);}finally{setIsLoading(false);// Always set loading state to false after fetching}};fetchUserData();},[]);return(<UserAuthContext.Providervalue={{user,setUser,isLoading}}>{children}</UserAuthContext.Provider>);};
TheUserAuthContext
component does the following:
- Context Setup: Create a context named
UserAuthContext
to hold user data and related functions. - UserProvider Component: This component provides user data and loading state to the children components.
- Fetching User Data:
useEffect
fetches user data on component mount and updates state. - Providing Context:
UserProvider
exposes user data, update function, and loading state. - Wrapping Your Application: Wrap your app with
UserProvider
to make context accessible everywhere.
User Registration functionality
Let's build the user registration functionality by adding code to our existing Register ComponenthandleRegister
function
consthandleRegister=async(e)=>{setLoadingStatus(true)e.preventDefault();try{// Call Appwrite function to handle user registrationif(password!==confirmPassword){alert("Passwords do not match");setLoadingStatus(false)return;}if(username===""||email===""||password===""||confirmPassword===""){alert("Please fill in all fields");setLoadingStatus(false);return;}// appwrite Register functionality 👇if(password.length<8){alert("Password must contain 8 characters");setLoadingStatus(false);return;}constpromise=account.create(ID.unique(),email,password,username);promise.then(function(response){console.log(response);// Successalert("Account Created Successfully 🚀");navigate("/login");},function(error){console.log(error);// Failurealert(error);});}catch(err){alert(err.message);}};
The updatedhandleSubmit
function code above handles user registration with Appwrite. Here's a breakdown of its functionality
- Password Validation: Checks if password length meets the minimum requirement (8 characters) and displays an error message if not.
- Appwrite integration: Calls Appwrite's
account.create
function to register the user with provided credentials - Success Handling: Logs the successful registration response, displays a success message(alert), and redirects the user to the login page
- Error Handling: Logs any errors encountered during registration and displays an error message to the user
User Login functionality
Let's build the user login functionality by adding code to our existing Login ComponenthandleLogin
function
consthandleLogin=async(e)=>{setButtonLoading(true)e.preventDefault();if(password===""||email===""){alert("Please fill in the field required");setButtonLoading(false)return;}// appwrite Login functionality 👇// Call Appwrite function to handle login with email and passwordconstpromise=account.createEmailPasswordSession(email,password);promise.then(function(response){console.log(response);// SuccesssetUser(response);navigate("/")setButtonLoading(false)},function(error){console.log(error);// Failurealert(error.message)setButtonLoading(false);});};
ThehandleLogin
updated function does the following:
- Appwrite Login: Uses
account.createEmailPasswordSession
to attempt login with email and password - Success: Logs the user data, updates the state using the
setUser
setter function, redirects the user to the homepage, and disables the loading indicator (setButtonLoading(false)
). - Failure: In the case of a failure, it logs the errors, displays the error message as an alert, and disables the loading indicator
We have now finished the Authentication section. Let's proceed to make the Private routes
Private Routes
Go ahead to create a file namedPrivateRoute.jsx
and then put the following code inside it
import{useContext}from"react";import{Outlet,Navigate}from"react-router-dom";import{UserAuthContext}from"../context/UserAuthContext";constPrivateRoute=()=>{const{user}=useContext(UserAuthContext);returnuser?<Outlet/>:<Navigateto={"/login"}/>;};exportdefaultPrivateRoute;
ThePrivateRoute
component is responsible for the following:
- The component utilizes the
UserAuthContext
created earlier to check if a user is currently logged in. - It retrieves the
user
data from the context using theuseContext
hook - If the
user
data exists(logged in), it renders the child component wrapped by . This allows you to define protected routes within your application and the route is only accessible by logged-in users. - If
user
isnull
(not logged in), it redirects the user to the login page(/login
) using theNavigate
fromreact-router-dom
.
Now let's us use thePrivateRoute
component in theApp
component
This is how the code will look like in our App component:
<Routeelement={<PrivateRoute/>}><Routepath="/"element={<Home/>}/></Route>
This allows us to simply usePrivateRoute
component we've just created.
TheHome
component which is a child component of thePrivateRoute
component appears if and only if theuser
object is not null
User Persistence
User persistence refers to holding user data even when the page is refreshed. This ensures that the user remains logged in or their preferences remembered even after a browser refresh or session termination.
Appwrite by default usesLocal Storage
for session management on Registering and Logging in users into our React applications
On refresh of theHome Page
, the application is routed to theLogin
page as the data from theUserAuthContext
hasn't been fetched yet.
To prevent this, let us add this piece of code to theLogin
page above thehandleLogin
function that checks and redirects the users back to theHome
page if the user object is not null or empty
import{useState,useContext,useEffect}from"react";import{Link}from"react-router-dom";import"./Login.css";import{account}from"../appwriteConfig";import{useNavigate}from"react-router-dom";import{UserAuthContext}from"../context/UserAuthContext";constLogin=()=>{const{setUser,user}=useContext(UserAuthContext)// console.log(user);constnavigate=useNavigate();const[email,setEmail]=useState("");const[password,setPassword]=useState("");const[buttonLoading,setButtonLoading]=useState(false);useEffect(()=>{if(user!==null){navigate("/");}},[user,navigate])consthandleLogin=async(e)=>{// Login logic. See above sections};
TheuseEFfect()
hook simply checks if the user isnot null
. If this evaluates to true, then the user is routed to the/
orHome
page.
Handling User Logout
We would handle the User Logout in theNavbar
component already created.
constNavbar=()=>{const{user,setUser}=useContext(UserAuthContext);constnavigate=useNavigate();consthandleLogin=()=>{navigate("/login");};consthandleLogout=async()=>{try{awaitaccount.deleteSession("current");setUser(null);navigate("/login");}catch(error){console.error(error);}};return(<nav><divclassName="navLogo">Logo</div><Linkto="/"className="navHomeLink">Home</Link>{user?(<buttononClick={handleLogout}className="navLoginButton">Logout</button>):(<buttononClick={handleLogin}className="navLoginButton">Login</button>)}</nav>);
In this UpdatedNavbar
component, we are doing the following:
- Conditional Rendering: We are conditionally rendering the Login and Logout buttons based on the current
user
value - Login: On click of the Login button, the user is routed to the
/login
page - Logout: On click of the Logout button, the
handleLogout()
is triggered. The current user session is then deleted and the user is changed to null using the setter functionsetUser(null)
. The user is then routed to the/login
page.
Here's a demo of how the app looks like:
Conclusion
In this article, we've explored and seen how to Authenticate users in our React apps using Appwrite. I'm delighted that you've followed along to this point.
Here is the github repo for this article incase you need to check it out,here
Lastly if you've found value in this article, please consider sharing it with your peers who may also benefit from it.
What are your thoughts on the topic of Authentication in React with Appwrite? Feel free to share your thoughts in the comments section below.
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse