Authentication in React with Appwrite
Tags: react, howto, tutorial
Published: 2024-03-24 14:23:40

## Table of Contents
- [Introduction](#intro)
- [React Project Setup with Appwrite] (#project-setup)
- [Building the User Interface(UI)] (#building-ui)
- [Set up React Context for User State Management] (#react-context)
- [User Registration functionality] (#user-registration)
- [User Login functionality] (#user-login)
- [Private Route] (#private-route)
- [User Persistence] (#user-persistence)
- [Handling User Logout] (#user-logout)
- [Conclusion] (#conclusion)

<a id="intro"></a>
## 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!

<a id="project-setup"></a>
## React Project Setup with Appwrite

To initiate our project setup, the first step involves registering and creating an account with [AppWrite](https://cloud.appwrite.io/register)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q1n8eenmfi6d06yxxm46.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4ofv14e5rralj9eltmzy.png)

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


![Register Hostname](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4gt7yduzgi16bczovjgu.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4l7n8qi81rn13n3sdwn8.png)

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. 

```bash
# 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


![Vite-Project-Scaffolding](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bwhs8acwh3ieepa8n9ck.png)

>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

```bash
  npm install appwrite
```

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

```yaml
  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

![project-id](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gnjqgoiby9vjt0wtx017.png)

Then create a `appwriteConfig.js` file in the `src` folder of your React app and paste the following code

```js
  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. 

<a id="building-ui"></a>
## 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
```js
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;
```

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

### Register Page
```js

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;

```

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

### Home Page
```js
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
```js
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;

```

```css
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: 


![Gif UI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6lwzykpnwrpqxtlyeedh.gif)

<a id="react-context"></a>
## 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.

```javascript
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. 

<a id="user-registration"></a>
## User Registration functionality
Let's build the user registration functionality by adding code to our existing Register Component `handleRegister` function

```javascript
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

<a id="user-login"></a>
## User Login functionality
Let's build the user login functionality by adding code to our existing Login Component `handleLogin` function

```javascript
 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

<a id="private-route"></a>
## Private Routes
Go ahead to create a file named `PrivateRoute.jsx` and then put the following code inside it

```javascript
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 the `useContext` hook
- If the `user` data exists(logged in), it renders the child component wrapped by <Outlet>. 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:

```javascript
<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

<a id="user-persistence"></a>
## 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

```javascript
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. 

<a id="user-logout"></a>
## Handling User Logout
We would handle the User Logout in the `Navbar` component already created. 

```javascript
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 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s67kp08xjh9r35su6iva.gif)

<a id="conclusion"></a>
## 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](https://github.com/DevYoma/react-appwrite-article)

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.