How to Build a Secure Task Management App with React and Appwrite

Introduction

Creating highly functional, secure, and scalable applications is now simpler than ever. As a developer, you don’t need to waste your time or resources setting up the backend; Appwrite can save you from any hassles required to set it up.

In this tutorial, I’ll walk you through how to create full-stack web applications with Appwrite. Upon completion, you’ll be able to leverage its features to create secure and fully-functional applications.

What is Appwrite?

Appwrite is an open-source backend platform that enables you to create secured and scalable (web and mobile) applications in a few minutes.

Appwrite

With Appwrite, you don’t need to worry about the backend resources of your application because it provides services such as serverless functions, authentication, database, file storage, and real-time communication.

It enables you to focus on developing your applications while it manages the crucial backend functionalities.

Why You Should Use Appwrite?

Appwrite is an amazing software that enables us to build full-stack applications. However, let’s see some unique features that make Appwrite stand out from other BaaS (Backend as a Service) platforms.

  • Open source

Appwrite is open-source, meaning its source code is freely available for inspection, modification, and contribution by the developer community. This openness fosters collaboration, growth, and transparency, allowing its users (developers) to contribute, fix and share any issues.

  • Excellent documentation

Appwrite documentation is simple and developer-friendly. Each concept is explained with code examples, making it easy for developers to understand and implement its various features. 

Whether you’re a new or existing user, Appwrite’s documentation ensures a smooth and efficient experience, allowing you to navigate and leverage its powerful capabilities.

  • A complete backend resource

Appwrite is a complete backend resource for your software applications. It provides a scalable and robust database that enables you to create complex relationships among data and perform CRUD and various query operations. 

It also provides file storage for storing and serving any file type, a complete authentication system for managing users within your application, and real-time communication between a server and multiple clients.

  • Fully customizable and extendable

Appwrite distinguishes itself among other Backend as a Service (BaaS) platforms by offering developers the flexibility to self-host their data. 

This unique feature allows you to choose between letting Appwrite manage all your backend resources or seamlessly migrating your data to other supporting platforms. 

Appwrite simplifies these migration processes by providing built-in data protection and encryption, ensuring a smooth and secure transition for your application’s data.

How to Add Appwrite to a React App

In this section, you’ll learn how to add Appwrite to a React application. Before we proceed, you need to create a React project.

Executing npx create-react-app <project-name> within your terminal will download the latest React version. However, this method is no longer recommended for installing React.

Instead, you can use Vite – a development environment for modern applications. 

Run npm create vite@latest, provide a project name, and select React as your preferred framework to create a new React project.

Appwrite

Next, visit the Appwrite homepage and create a new account.

Create an organization for your projects and proceed to add a new project under the organization.

appwrite

After creating the Appwrite project, select Web under the Add a platform section to add Appwrite to the React project.

react and Appwrite

Follow the on-screen steps.

In the image below, I assigned a name to the application and used an asterisk as the hostname. Once the application is deployed, you can easily update the hostname to match the application’s URL.

Appwrite

Install the Appwrite Node.js SDK into your React.js project by running the code snippet below.

npm install appwrite

Create a .env.local and an appwrite.ts file at the root of your project.

touch appwrite.ts .env.local

Copy the code below into the appwrite.ts file to connect the React project to Appwrite.

import { Client, Account, Databases } from "appwrite";
const projectID = import.meta.env.VITE_PROJECT_ID;

const client = new Client();
client.setEndpoint("https://cloud.appwrite.io/v1").setProject(projectID);

//👇🏻 enables us to use Appwrite authentication
export const account = new Account(client);

//👇🏻 enables us to use Appwrite database
export const db = new Databases(client);

//👇🏻 the database and collection ID
export const databaseID = import.meta.env.VITE_DB_ID;
export const collectionID = import.meta.env.VITE_COLLECTION_ID;

The code snippet above enables us to access and interact with the authentication and database features provided by Appwrite.

Copy the code below into the .env.local file.

VITE_PROJECT_ID=<Your_Appwrite_Project_ID>
VITE_DB_ID=<Your_Appwrite_Database_ID>
VITE_COLLECTION_ID=<Your_Appwrite_Collection_ID>

Select Databases from the sidebar menu to create a new database and a collection. Copy the IDs into the .env.local file.

appwrite backend

Congratulations! You’ve successfully added Appwrite to a React project.

You can now interact with Appwrite and perform various actions like CRUD operations and authentication.


Building the App User Interface

In this section, I’ll walk you through building the interface for a task planner application that authenticates users using Appwrite and allows them to create, retrieve, update, and delete tasks from the database.

The application is divided into four pages – the Login and Register page, the Home page, and the Dashboard page.

To navigate between these pages in React, you need to install the React Router library. It enables us to create various routes within our application and navigate seamlessly between them.

Run the code snippet below to install the React Router package.

npm install react-router-dom

Create a components folder containing all the page routes within the React src folder by running the code snippet below. The files represent the components for each page route.

cd src
mkdir components
cd components
touch Home.tsx Login.tsx Register.tsx Dashboard.tsx

Finally, update the App.tsx file to render each component on a particular route within the application.

import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "./components/home";
import Login from "./components/login";
import Register from "./components/register";
import Dashboard from "./components/dashboard";

export default function App() {
    return (
        <Router>
            <Routes>
                <Route path='/' element={<Home />} />
                <Route path='/login' element={<Login />} />
                <Route path='/register' element={<Register />} />
                <Route path='/dashboard' element={<Dashboard />} />
            </Routes>
        </Router>
    );
}

Congratulations! You’ve successfully created the different pages needed within the application.

Next, let’s design the user interface.


The Home Page

The Home component represents the application landing page. It shows brief information about the application and navigates users to the Login component.

import { Link } from "react-router-dom";
import note from "../../src/assets/note.jpg";

export default function Home() {
    return (
        <div className='w-full min-h-[100vh]'>
            <nav className='h-[10vh] flex items-center justify-between px-6 border-b-[1px] '>
                <h3 className='font-bold text-2xl'>TaskFlow</h3>
                <Link
                    to='/login'
                    className='bg-[#FC6736] py-3 px-8 rounded-md text-white'
                >
                    Sign in
                </Link>
            </nav>
            <div className='w-full h-[90vh] p-8 flex justify-between items-center'>
                <div className='w-1/2'>
                    <h2 className='text-4xl font-extrabold mb-4'>
                        Complete your <span className='text-[#FC6736]'>tasks</span> like a
                        pro
                    </h2>
                    <p className='mb-3'>
                        TaskFlow simplifies task management for effortless productivity.
                        Organize, prioritize, and execute your tasks seamlessly.{" "}
                    </p>

                    <p className='mb-3'>
                        Whether you're an individual or part of a team, TaskFlow adapts to
                        your workflow. Navigate through your to-dos effortlessly and stay
                        ahead of deadlines. With a user-friendly interface and smart
                        features, TaskFlow makes productivity clear and straightforward.
                    </p>
                    <Link
                        to='/login'
                        className='bg-[#FC6736] p-4 text-lg w-[200px] text-center rounded-md text-white block mt-3'
                    >
                        Get Started
                    </Link>
                </div>
                <div className='w-1/2 flex items-center justify-center'>
                    <img
                        src={note}
                        alt='note'
                        className='w-[500px] h-[500px] object-cover'
                    />
                </div>
            </div>
        </div>
    );
}
appwrite-task-flow

The Login page

The login page displays a form that accepts users’ email and password and enables them to sign in to the application.

import { useState } from "react";
import { Link, useNavigate } from "react-router-dom";

export default function Login() {
    const [email, setEmail] = useState<string>("");
    const [password, setPassword] = useState<string>("");
    const navigate = useNavigate();

    const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        console.log({ email, password });
        navigate("/dashboard");
    };

    return (
        <div className='w-full min-h-[100vh] flex flex-col items-center justify-center p-8'>
            <form className='flex flex-col md:w-[60%] w-full' onSubmit={handleSubmit}>
                <h2 className='font-bold text-3xl text-center mb-2'>Log in</h2>
                <label htmlFor='email'>Email Address</label>
                <input
                    type='text'
                    id='email'
                    required
                    value={email}
                    onChange={(e) => setEmail(e.target.value)}
                    className='border-[1px] px-4 py-3 rounded mb-2'
                />
                <label htmlFor='password'>Password</label>
                <input
                    type='password'
                    id='password'
                    required
                    value={password}
                    onChange={(e) => setPassword(e.target.value)}
                    className='border-[1px] px-4 py-3 rounded mb-3'
                />
                <button className='bg-orange-500 hover:bg-orange-700 text-white font-bold p-3 rounded text-lg mb-2'>
                    Log in
                </button>
                <p className='text-center'>
                    Don't have an account?{" "}
                    <Link to='/register' className='text-orange-500'>
                        Create one
                    </Link>
                </p>
            </form>
        </div>
    );
}

appwrite-login

The Register Page

The Register page displays a form that allows users to create an account within the application. The form accepts the user’s full name, email, and password.

import { useState } from "react";
import { Link, useNavigate } from "react-router-dom";

export default function Register() {
    const [fullName, setFullName] = useState<string>("");
    const [email, setEmail] = useState<string>("");
    const [password, setPassword] = useState<string>("");
    const [cpassword, setCpassword] = useState<string>("");
    const navigate = useNavigate();

    const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        console.log({ fullName, email, password, cpassword });
        navigate("/login");
    };

    return (
        <div className='w-full min-h-[100vh] flex flex-col items-center justify-center p-8'>
            <form className='flex flex-col md:w-[60%] w-full' onSubmit={handleSubmit}>
                <h2 className='font-bold text-3xl text-center mb-2'>Register</h2>
                <label htmlFor='username'>Full Name</label>
                <input
                    type='text'
                    id='fullName'
                    className='border-[1px] px-4 py-3 rounded mb-2'
                    value={fullName}
                    required
                    onChange={(e) => setFullName(e.target.value)}
                />
                <label htmlFor='email'>Email Address</label>
                <input
                    type='email'
                    id='email'
                    className='border-[1px] px-4 py-3 rounded mb-2'
                    value={email}
                    required
                    onChange={(e) => setEmail(e.target.value)}
                />
                <label htmlFor='password'>Password</label>
                <input
                    type='password'
                    id='password'
                    className='border-[1px] px-4 py-3 rounded mb-3'
                    value={password}
                    required
                    onChange={(e) => setPassword(e.target.value)}
                />
                <label htmlFor='cpassword'>Confirm Password</label>
                <input
                    type='password'
                    id='cpassword'
                    className='border-[1px] px-4 py-3 rounded mb-3'
                    value={cpassword}
                    required
                    onChange={(e) => setCpassword(e.target.value)}
                />

                <button className='bg-orange-500 hover:bg-orange-700 text-white font-bold p-3 rounded text-lg mb-2'>
                    Create account
                </button>
                <p className='text-center'>
                    Already have an account?{" "}
                    <Link to='/login' className='text-orange-500'>
                        Log in
                    </Link>
                </p>
            </form>
        </div>
    );
}

appwrite-app-react

The Dashboard Page

The Dashboard page displays the tasks based on their current status, either pending or completed. It also allows users to update the tasks’ status and delete any tasks. 

Users can add new tasks and log out of the application from the Dashboard page.

import { useEffect, useState, useCallback } from "react";
import { useNavigate } from "react-router-dom";


interface Task {
    $id: string;
    title?: string;
    completed?: boolean;
    userID?: string;
}

export default function Dashboard() {
    const [pendingTasks, setPendingTasks] = useState<Task[]>([
        { $id: "", title: "", completed: false, userID: "" },
    ]);
    const [completedTasks, setCompletedTasks] = useState<Task[]>([
        { $id: "", title: "", completed: false, userID: "" },
    ]);
    const [loading, setLoading] = useState<boolean>(true);
    const [toggleView, setToggleView] = useState<boolean>(false);
    const navigate = useNavigate();

    return (
        <div className='w-full min-h-[100vh]'>
            <nav className='h-[10vh] flex items-center justify-between px-6 border-b-[1px] '>
                <h3 className='font-bold text-2xl'>TaskFlow</h3>
                <div className='space-x-4'>
                    <button
                        className='bg-blue-500 py-3 px-4 rounded-md text-white'
                    >
                        Add new task
                    </button>
                    <button
                        className='bg-[#FC6736] py-3 px-8 rounded-md text-white'
                    >
                        Log out
                    </button>
                </div>
            </nav>
            <main className='w-full min-h-[90vh] p-8'>
                <div className='flex items-center w-full justify-center space-x-6 mb-5'>
                    <button
                        className={`${
                            toggleView ? "bg-red-100" : "bg-red-700 text-red-50"
                        }  flex items-center justify-center p-8 text-center text-xl cursor-pointer shadow-md rounded`
                    >
                        Pending tasks
                    </button>
                    <div
                        className={`${
                            toggleView ? "bg-green-700 text-green-50" : "bg-green-100"
                        }  flex items-center justify-center p-8 text-center text-xl cursor-pointer shadow-md rounded`}
                    >
                        Completed tasks
                    </div>
                </div>

                <h2 className='font-semibold opacity-65 text-xl mb-3'>
                    {toggleView ? "Completed Tasks" : "Pending Tasks"}
                </h2>
                <div>
                    {/** --- task contents --*/}
                </div>
            </main>

        </div>
    );
}

react-appwrite

How to Authenticate Users with Appwrite

Appwrite provides various forms of authentication, such as email and password, magic URLs, phone numbers, social logins, and numerous OAuth2 providers. 

In this section, you’ll learn how to add email and password authentication to a React application.

Signing up New Users

To log users into the application, they need to create an account using the Appwrite SDK initialized within the React project.

Therefore, import account from the appwrite.ts file into the Register component.

import { Client, Account, Databases } from "appwrite";
const projectID = import.meta.env.VITE_PROJECT_ID;

const client = new Client();
client.setEndpoint("https://cloud.appwrite.io/v1").setProject(projectID);

//👇🏻 enables us to use Appwrite authentication
export const account = new Account(client);

//👇🏻 enables us to use Appwrite database
export const db = new Databases(client);

//👇🏻 the database and collection ID
export const databaseID = import.meta.env.VITE_DB_ID;
export const collectionID = import.meta.env.VITE_COLLECTION_ID;

Execute the createUser function below when a user submits the Register form.

import { useNavigate } from "react-router-dom";
import { account } from "../../appwrite";
import { ID } from "appwrite";

const navigate = useNavigate();

const createUser = async (
    email: string,
    password: string,
    fullName: string
) => {
    try {
        await account.create(ID.unique(), email, password, fullName);
        alert("Account created successfully ✅");
        naviagate("/login");
    } catch (err) {
        alert("Check your network / User already exists ❌");
        console.log(err);
    }
};

The code snippet above creates an account using the user’s email, password, and full name. Appwrite creates a unique ID for each user using the ID method. Upon creation, the application redirects the user to the login page. Otherwise, Appwrite returns an error if there is an issue.

Signing in Existing Users

Appwrite also allows you to log users into the application using the Account method. 

Import `account` from the `appwrite.ts` file and execute the function below when a user submits the login form.

import { account } from "../../appwrite";

const loginUser = async (email: string, password: string) => {
    try {
        await account.createEmailSession(email, password);
        navigate("/dashboard");
    } catch (err) {
        console.error(err);
        alert("Invalid credentials ❌");
    }
};

The code snippet above verifies the user’s email and password to ensure before granting access to the application. It also creates a session for the user. 

Appwrite uses the browser’s local storage to manage the user’s session, enabling us to differentiate authenticated users from unauthenticated ones.

Logging Users Out

To log users out of the application, Appwrite allows you to delete users’ session using the code snippet below:

import { account } from "../../appwrite";

const logOut = async () => {
    try {
        await account.deleteSession("current");
        navigate("/login");
    } catch (err) {
        console.error(err);
        alert("Encountered an error 😪");
    }
};

The logOut function deletes the user’s session and redirects the user to the login page.

Protecting Pages from Unauthenticated Users

The Dashboard page enables authenticated users to preview the existing tasks and update and add new tasks. To prevent unauthenticated users from viewing the page, you need to protect the Dashboard page by ensuring that only logged-in users can view the page.

To do this, Appwrite allows us to retrieve the current user’s credentials, and it returns null if a user is not authenticated.

Therefore, add a useEffect hook that returns the current user’s data from Appwrite. If the data exists, the user is authenticated and can view the Dashboard page.

import { account } from "../../appwrite";

useEffect(() => {
    const checkAuthStatus = async () => {
        try {
            const userData = await account.get();
            console.log(userData);
            setLoading(false);
        } catch (err) {
            console.error(err);
            navigate("/login");
        }
    };
    checkAuthStatus();
}, []);

Congratulations! You’ve successfully learnt how to authenticate users with Appwrite.

In the upcoming sections, I’ll walk you through how to fetch, delete, create, and update tasks from the Appwrite database.


How to Perform CRUD Operations with Appwrite Database

Here, you’ll learn how to interact with the Appwrite database by performing CRUD (Create, Update, and Delete) operations within the application.

Ensure you have created a database and a collection for your data, and select Attributes from the top menu to add the data attributes to Appwrite. 

appwrite-curd-operations

From the image above, each task has three attributes. The title represents the task name, completed is of boolean data type – describes the status of a task, and the userID is the id of the user who created the task. The user ID is used for identifying a user’s tasks.

Next, select Settings from the top menu, scroll down to the Permissions section, and give all the users permission to read, create, update, and delete data from the collection.

CRUD Operations with Appwrite Database

Saving data to the database

You can save data containing the same attributes to the database collection as shown below.

import { db, collectionID, databaseID } from "../../appwrite";
import { ID } from "appwrite";

const createTask = async () => {
    try {
        await db.createDocument(databaseID, collectionID, ID.unique(), {
            title: task,
            completed: false,
            userID,
        });
        alert("Task created 🎉");
    } catch (error) {
        console.error("DB ERROR >>", error);
        alert("Encountered an error ❌");
    }
};

The code snippet above accepts the database and collection IDs and adds the newly created data to the database collection. 

You can execute this function when a user adds a new task. The user’s ID can be retrieved from the account.get() method.

Retrieving Data from Appwrite

Appwrite allows you to retrieve data and make queries to the database easily. The code snippet below returns the pending and completed tasks from Appwrite

The asynchronous function – db.listDocuments accepts three parameters – the database ID, the collection ID, and an array containing the query parameters.

import { account, db, databaseID, collectionID } from "../../appwrite";
import { Query } from "appwrite";

const fetchPendingTasks = async () => {
    try {
        const pendingTasks = await db.listDocuments(databaseID, collectionID, [
            Query.equal("userID", userID),
            Query.equal("completed", false),
        ]);
        return pendingTasks.documents;
    } catch (err) {
        console.error(err);
        return null;
    }
};

const fetchCompletedTasks = async () => {
    try {
        const completedTasks = await db.listDocuments(databaseID, collectionID, [
            Query.equal("userID", userID),
            Query.equal("completed", true),
        ]);
        return completedTasks.documents;
    } catch (err) {
        console.error(err);
        return null;
    }
};

Updating Data with Appwrite

When a user completes a task, you can update its status by modifying the completed attribute. 

The db.updateDocument() method accepts the task’s ID and the attributes to be updated as parameters. 

The code snippet below updates a task’s status and marks it as completed.

import { db, databaseID, collectionID } from "../../appwrite";

const markasCompleted = async () => {
    try {
        await db.updateDocument(databaseID, collectionID, taskID, {
            completed: true,
        });
        alert("Task updated 🎉");
    } catch (err) {
        console.error(err); // Failure
        alert("Encountered an error 😪");
    }
};

Deleting Data from Appwrite

To delete a task, you can select it using its ID, and Appwrite permanently deletes it from the database.

const handleDeleteTask = async () => {
    try {
        await db.deleteDocument(databaseID, collectionID, taskID);
    } catch (err) {
        console.error(err);
        alert("Encountered an error 😪");
    }
};

Congratulations! You’ve completed the project for this tutorial.


Conclusion

So far, you’ve learnt what Appwrite is, why you should use it, and how to leverage its authentication and database features to build secured and scalable full-stack applications.

If you are looking forward to shipping great software products or side projects, you should consider Appwrite.

Additionally, Appwrite is open-source. Therefore, you can join the developer community by contributing code and interacting with the maintainers to improve the software.

Thank you for reading and If you enjoyed this blog and want to learn more, check out my other articles.

Leave a Comment

Your email address will not be published. Required fields are marked *