Authentication in React with Appwrite

devyoma

Emore Ogheneyoma Lawrence

Posted on March 24, 2024

Authentication in React with Appwrite

Table of Contents

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:

Create Appwrite Project

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

Web Platform

On Click of the Web Platform we are presented with a screen in the screenshot below.

Register Hostname

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.

Project setup complete

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


Enter fullscreen mode Exit fullscreen mode

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

Vite-Project-Scaffolding

Entering ./ as the project name in Vite scaffolds the React project in your current terminal directory. Choose React from the framework options, and select the Javascript variant.
After creating the project, npm install fetches the necessary tools and npm 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


Enter fullscreen mode Exit fullscreen mode

Next, create a .env.local file in the project root directory and paste the following



  VITE_APPWRITE_PROJECT_ID="PROJECT_ID"


Enter fullscreen mode Exit fullscreen mode

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

project-id

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


Enter fullscreen mode Exit fullscreen mode

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:

  1. Login Page: Initial access point for users to input credentials and log in
  2. Register Page: Allows new users to create accounts by providing essential details.
  3. Home Page (Private Route): Authenticated users' exclusive space, displaying user email and ID from Appwrite.
  4. 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;


Enter fullscreen mode Exit fullscreen mode


.loginPage > form > *:not(:last-child) {
  margin-bottom: 1rem;
}


Enter fullscreen mode Exit fullscreen mode

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;



Enter fullscreen mode Exit fullscreen mode


.registerPage > form > *:not(last-child) {
  margin-bottom: 1rem;
}


Enter fullscreen mode Exit fullscreen mode

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;



Enter fullscreen mode Exit fullscreen mode

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;



Enter fullscreen mode Exit fullscreen mode


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


Enter fullscreen mode Exit fullscreen mode

If you've followed the UI code snippets, you'll have a basic app that looks like this:

Gif UI

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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

  };


Enter fullscreen mode Exit fullscreen mode

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;


Enter fullscreen mode Exit fullscreen mode

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 the useContext 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 is null (not logged in), it redirects the user to the login page(/login) using the Navigate from react-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>


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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 function setUser(null). The user is then routed to the /login page.

Here's a demo of how the app looks like:

App-UI-demo

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.

💖 💪 🙅 🚩
devyoma
Emore Ogheneyoma Lawrence

Posted on March 24, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related