Authentication in React with Appwrite
Emore Ogheneyoma Lawrence
Posted on March 24, 2024
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 with AppWrite
After registering, create a new project in appwrite by clicking on the Create 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 use react-appwrite-auth
for this example. Don't worry about the Project ID, it will be automatically generated for you when you click Next
On click of the Next
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 the Create
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 the Web
Platform
On Click of the Web Platform
we are presented with a screen in the screenshot below.
These two fields are required. Set the name as React Appwrite Auth
and the hostname as localhost
. Alternatively, you can use *
as your hostname.
We would then see two optional steps that guide us on how to Install the appwrite SDK into our app
and Initialize 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 named react-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.
# npm
npm create vite@latest
# Yarn
yarn 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
npm install appwrite
Next, create a .env.local
file in the project root directory and paste the following
VITE_APPWRITE_PROJECT_ID="PROJECT_ID"
To obtain your PROJECT_ID
, navigate to your project created on appwrite, in our case, navigate to the react-appwrite-auth
project, you will be able to see it here.
You can also navigate to the react-appwrite-auth
project dashboard > settings
, you will be able to see your PROJECT_ID similar to the screenshot below
Then create a appwriteConfig.js
file in the src
folder of your React app and paste the following code
import { Account, Client } from "appwrite"; // Import Client from "appwrite"
const client = new Client(); // Create a new Client instance
client
.setEndpoint("https://cloud.appwrite.io/v1")
.setProject(import.meta.env.VITE_APPWRITE_PROJECT_ID)
export const account = new Account(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";
const Login = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [buttonLoading, setButtonLoading] = useState(false);
const handleLogin = async (e) => {
setButtonLoading(true)
e.preventDefault();
if (password === "" || email === "") {
alert("Please fill in the field required")
setButtonLoading(false)
return
}
// appwrite Login functionality 👇
return (
<div className="loginPage">
<h2>Login</h2>
<form onSubmit={handleLogin}>
<div>
<label htmlFor="email">Email:</label>
<input
type="email"
required
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<label htmlFor="password">Password:</label>
<input
type="password"
required
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<button type="submit">{buttonLoading ? "Loading..." : "Login"}</button>
<div>
Dont have an account? <Link to="/register">Register</Link>
</div>
</form>
</div>
);
};
export default Login;
.loginPage > form > *:not(:last-child) {
margin-bottom: 1rem;
}
Register Page
import { useState } from "react";
import { Link } from "react-router-dom";
import "./Register.css";
const Register = () => {
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [loadingStatus, setLoadingStatus] = useState(false);
const handleRegister = async (e) => {
e.preventDefault();
try {
// Call Appwrite function to handle user registration
if (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 (
<div className="registerPage">
<h2>Register</h2>
<form onSubmit={handleRegister}>
<div>
<label htmlFor="username">Username:</label>
<input
type="text"
required
id="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div>
<label htmlFor="email">Email:</label>
<input
type="email"
required
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<label htmlFor="password">Password:</label>
<input
type="password"
required
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div>
<label htmlFor="confirmPassword">Confirm Password:</label>
<input
type="password"
required
id="confirmPassword"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
</div>
<button type="submit">{ <button type="submit">{loadingStatus ? "Loading..." : "Register"}</button>
}</button>
<div>
Have an account? <Link to="/login">Login</Link>
</div>
</form>
</div>
);
};
export default Register;
.registerPage > form > *:not(last-child) {
margin-bottom: 1rem;
}
Home Page
import "./Home.css";
const Home = () => {
return (
<div>
<h1>Home Page</h1>
<p>This page is a Protected Page and should only be seen by Authenticated Users</p>
</div>
);
};
export default Home;
Navbar Component
import { Link } from "react-router-dom";
import "./Navbar.css";
import { useNavigate } from "react-router-dom";
const Navbar = () => {
const navigate = useNavigate();
const handleLogin = () => {
navigate("/login")
}
return (
<nav>
<div className="navLogo">Logo</div>
<Link to="/" className="navHomeLink">Home</Link>
<button onClick={handleLogin} className="navLoginButton">Login</button>
</nav>
);
};
export default Navbar;
nav {
/* max-width: 768px; */
margin-inline: auto;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 3px solid black;
}
nav > .navLogo {
font-size: 2.5rem;
}
nav > .navHomeLink {
font-size: 1.4rem;
}
nav > button {
background-color: transparent;
border: 1px solid gray;
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 a context
folder. Inside it, add a file named UserAuthContext.jsx
. Paste the provided code into this file to handle user authentication efficiently.
import { createContext, useEffect, useState } from "react";
import { account } from "../appwriteConfig";
export const UserAuthContext = createContext();
export const UserProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true); // Track loading state
useEffect(() => {
const fetchUserData = async () => {
try {
const response = await account.get(); // Fetch user data
setUser(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.Provider value={{ user, setUser, isLoading }}>
{children}
</UserAuthContext.Provider>
);
};
The UserAuthContext
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 Component handleRegister
function
const handleRegister = async (e) => {
setLoadingStatus(true)
e.preventDefault();
try {
// Call Appwrite function to handle user registration
if (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;
}
const promise = account.create(ID.unique(), email, password, username);
promise.then(
function (response) {
console.log(response); // Success
alert("Account Created Successfully 🚀");
navigate("/login");
},
function (error) {
console.log(error); // Failure
alert(error);
}
);
} catch (err) {
alert(err.message);
}
};
The updated handleSubmit
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 Component handleLogin
function
const handleLogin = 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 password
const promise = account.createEmailPasswordSession(email, password);
promise.then(
function (response) {
console.log(response); // Success
setUser(response);
navigate("/")
setButtonLoading(false)
},
function (error) {
console.log(error); // Failure
alert(error.message)
setButtonLoading(false);
}
);
};
The handleLogin
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 named PrivateRoute.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";
const PrivateRoute = () => {
const { user } = useContext(UserAuthContext);
return user ? <Outlet /> : <Navigate to={"/login"} />;
};
export default PrivateRoute;
The PrivateRoute
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 the PrivateRoute
component in the App
component
This is how the code will look like in our App component:
<Route element={<PrivateRoute />}>
<Route path="/" element={<Home />} />
</Route>
This allows us to simply use PrivateRoute
component we've just created.
The Home
component which is a child component of the PrivateRoute
component appears if and only if the user
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 uses Local Storage
for session management on Registering and Logging in users into our React applications
On refresh of the Home Page
, the application is routed to the Login
page as the data from the UserAuthContext
hasn't been fetched yet.
To prevent this, let us add this piece of code to the Login
page above the handleLogin
function that checks and redirects the users back to the Home
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";
const Login = () => {
const { setUser, user } = useContext(UserAuthContext)
// console.log(user);
const navigate = useNavigate();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [buttonLoading, setButtonLoading] = useState(false);
useEffect(() => {
if(user !== null){
navigate("/");
}
}, [user, navigate])
const handleLogin = async (e) => {
// Login logic. See above sections
};
The useEFfect()
hook simply checks if the user is not null
. If this evaluates to true, then the user is routed to the /
or Home
page.
Handling User Logout
We would handle the User Logout in the Navbar
component already created.
const Navbar = () => {
const { user, setUser } = useContext(UserAuthContext);
const navigate = useNavigate();
const handleLogin = () => {
navigate("/login");
};
const handleLogout = async () => {
try {
await account.deleteSession("current");
setUser(null);
navigate("/login");
} catch (error) {
console.error(error);
}
};
return (
<nav>
<div className="navLogo">Logo</div>
<Link to="/" className="navHomeLink">
Home
</Link>
{user ? (
<button onClick={handleLogout} className="navLoginButton">
Logout
</button>
) : (
<button onClick={handleLogin} className="navLoginButton">
Login
</button>
)}
</nav>
);
In this Updated Navbar
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.
Posted on March 24, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.