How to Build a Full Stack Notes App Using React and Supabase – The Complete Guide

Introduction

Most web and mobile applications are built using full-stack technologies.

For example, when you visit a movie streaming platform, two components are involved: the client-side (frontend) and the server-side (backend).

The interface you interact with to select, play, and pause a movie is known as the frontend, while the backend is invisible to the user. The backend ensures that the recommendation list matches your interest, a selected movie pauses and plays as expected, and many other functionalities.

When you build software using backend technologies, such as Node.js, Golang, databases, web servers, etc, and a frontend technology, such as React, Vuejs, etc, the process is called Full Stack Development.

In this tutorial, I’ll walk you through how to build full-stack web applications using React and Supabase.

React will handle the client side, while Supabase manages all backend logic, including authentication and database storage.

What is Supabase?

Supabase

Supabase is an open-source Firebase alternative that enables you to create secured and scalable software applications within a few minutes.

With Supabase, you don’t need to worry about the backend resources for your application.

Supabase provides a secured Postgres database, a complete user management system that handles various forms of authentication, including email and password, email sign-in, and social authentication, a file storage system that enables us to store and serve files of any size, real-time communication, and many others.

Why You Should Use Supabase?

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

  • Excellent documentation

Supabase 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, Supabase’s documentation ensures a smooth and efficient experience, allowing you to navigate and leverage its powerful capabilities.

  • A complete backend resource

Supabase is a complete backend resource for your software applications. It provides an extendable and secured Postgres 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, real-time communication between a server and multiple clients, edge functions, and a vector database for AI applications.

  • Open source

Supabase 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.

  • Versatile and Adaptable

Supabase client SDK is available in various languages, such as JavaScript, Flutter, C#, Swift, and Python. You can leverage Supabase to build modern and scalable software applications if you are using any of the programming languages. 

Supabase also allows you to easily migrate your existing data, auth, and storage from other platforms (Firebase, Heroku, Render, MySQL, Postgres, etc). You can also integrate APIs, authentication, dev tools, and data platforms into Supabase. 

Depending on your backend resource, Supabase provides a simple how-to guide that enables you to migrate and integrate your resources into the platform.


How to Add Supabase to a React App

In this section, you’ll learn how to add Supabase 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.

React

To add Supabase to the newly created React project, visit the Supabase website and create an account.

Create a new Supabase organization and a project, as shown below:

How to create a new project in Supabase

Click the Settings icon on the sidebar and select API to copy the project URL and the public API key.

API settings-Supabase

Save the project URL and public API key in a .env.local file within the React project.

VITE_SUPABASE_URL=<your_supabase_URL>
VITE_SUPABASE_ANON_KEY=<your_supabase_ANON_key>

Next, install the Supabase JavaScript client library by running the code snippet below.

npm install @supabase/supabase-js

Create a supabaseClient.ts file at the root of your project folder and copy the code below into the file to create a connection with Supabase using the URL and public key.

import { createClient } from "@supabase/supabase-js";

const supaseURL = import.meta.env.VITE_SUPABASE_URL;
const AnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;

export const supabase = createClient(supaseURL, AnonKey);

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

You can now interact with Supabase and perform various actions such as CRUD operations, authentication, file storage, etc.


Building the App User Interface

To demonstrate how to use some of the features provided by Supabase, I’ll walk you through building a note-taking application that authenticates users via Supabase and allows them to create, retrieve, update, and delete notes from the database.

Before we go into how to add the functionalities, let’s design the application interface.

The application is divided into six (6) pages which are:

  • landing page
  • login page
  • signup page
  • dashboard page
  • page that shows each note
  • page that creates new notes.

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 React Router.

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 below represent the components for each page route.

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

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

import { Routes, Route, BrowserRouter as Router } from "react-router-dom";
import Home from "./components/Home";
import Login from "./components/Login";
import Register from "./components/Register";
import Dashboard from "./components/Dashboard";
import Content from "./components/Content";
import CreateContent from "./components/CreateContent";

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 />} />
                <Route path='/notes/:slug' element={<Content />} />
                <Route path='/create' element={<CreateContent />} />
            </Routes>
        </Router>
    );
}

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

Next, let’s design the user interface.


The Home Component

The Home component represents the application landing page.

It shows brief information about the application and navigates users to the Login component.

import { useNavigate } from "react-router-dom";

export default function App() {
    const navigate = useNavigate();

    return (
        <div className='min-h-screen w-full flex flex-col items-center justify-center md-w-[70%] p-8'>
            <h2 className='text-4xl text-center font-extrabold text-[#304D30] mb-2'>
                Note-Keep
            </h2>
            <div className='w-[70%] mb-4'>
                <p className='text-center opacity-60'>
                    Say goodbye to scattered notes and hello to a more organized life.
                </p>
                <p className='text-center opacity-60'>
                    Noke-Keep offers a versatile platform for all your note-taking needs,
                    helping you stay focused and in control.
                </p>
            </div>
            <button
                onClick={() => navigate("/login")}
                className='bg-[#304D30] text-[#EEF0E5] px-4 py-3 rounded-md w-[200px] font-bold'
            >
                SIGN IN
            </button>
        </div>
    );
}
Full-Stack-Development-React-Supabase

The Login component

The Login component displays a form that enables users to provide their email and password before they can use the application.

It authenticates the user’s credentials and determines whether they should be granted access to the application.

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

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

    return (
        <div className='h-screen w-full flex'>
            <section className='md:w-[60%] w-full p-8 flex flex-col justify-center'>
                <h2 className=' text-3xl mb-8 font-bold text-[#304D30]'>Log in</h2>
                <form className='w-full mb-6' onSubmit={handleSubmit}>
                    <label>Email Address</label>
                    <input
                        type='email'
                        className='w-full rounded-md border border-gray-400 py-2 px-4 mb-6'
                        value={email}
                        required
                        onChange={(e) => setEmail(e.target.value)}
                    />

                    <label>Password</label>
                    <input
                        type='password'
                        className='w-full rounded-md border border-gray-400 py-2 px-4 mb-4'
                        value={password}
                        required
                        onChange={(e) => setPassword(e.target.value)}
                    />
                    <button className='bg-[#304D30] text-white px-8 py-4 rounded-md'>
                        LOG IN
                    </button>
                </form>
                <p>
                    Don't have an account?{" "}
                    <Link to='/register' className='text-[#5C8374]'>
                        Register
                    </Link>
                </p>
            </section>
            <div className='md:w-[40%] hidden md:inline-block'>
                <img
                    src='https://source.unsplash.com/8eSrC43qdro'
                    alt='Login'
                    className='object-cover w-full h-full'
                />
            </div>
        </div>
    );
}

The code snippet above displays a form that accepts the user’s email and password and logs them to the console when the form is submitted. You’ll learn how to validate and authenticate users with Supabase later on.

Login Component- Supabase

The Register Component

The Register component 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 { useNavigate, Link } from "react-router-dom";
import { useState } from "react";

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

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

    return (
        <div className='h-screen w-full flex'>
            <section className='md:w-[60%] w-full p-8 flex flex-col justify-center'>
                <h2 className=' text-3xl mb-8 font-bold text-[#304D30]'>Register</h2>
                <form className='w-full mb-6' onSubmit={handleSubmit}>
                    <label>Full Name</label>
                    <input
                        type='text'
                        className='w-full rounded-md border border-gray-400 py-2 px-4 mb-3'
                        value={fullName}
                        required
                        onChange={(e) => setFullName(e.target.value)}
                    />

                    <label>Email Address</label>
                    <input
                        type='email'
                        className='w-full rounded-md border border-gray-400 py-2 px-4 mb-3'
                        value={email}
                        required
                        onChange={(e) => setEmail(e.target.value)}
                    />

                    <label>Password</label>
                    <input
                        type='password'
                        className='w-full rounded-md border border-gray-400 py-2 px-4 mb-3'
                        value={password}
                        required
                        onChange={(e) => setPassword(e.target.value)}
                        minLength={7}
                    />
                    <button className='bg-[#304D30] text-white px-8 py-4 rounded-md'>
                        REGISTER
                    </button>
                </form>
                <p>
                    Already have an account?{" "}
                    <Link to='/login' className='text-[#5C8374]'>
                        Login
                    </Link>
                </p>
            </section>
            <div className='md:w-[40%] hidden md:inline-block'>
                <img
                    src='https://source.unsplash.com/-q69Jfp6MtM'
                    alt='Login'
                    className='object-cover w-full h-full'
                />
            </div>
        </div>
    );
}

The code snippet above renders a form that accepts the user’s full name, email, and password and logs them to the console.

Supabase-full-stack-app

The Dashboard Component

The Dashboard component displays the available notes and the buttons for adding new notes and logging out of the application.

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

export default function Dashboard() {
    const navigate = useNavigate();

    const handleLogout = async () => {
        navigate("/");
    };

    return (
        <div className='w-full'>
            <nav className='px-8 h-[10vh] bg-[#304D30] w-full flex items-center justify-between'>
                <Link to='/dashboard' className='text-2xl font-bold text-[#EEE7DA]'>
                    Note-Keep
                </Link>
                <button
                    className='p-3 bg-red-400 text-white px-4 rounded-sm'
                    onClick={() => handleLogout()}
                >
                    Log out
                </button>
            </nav>

            <main className='p-8 w-full '>
                <header className='flex items-center justify-between'>
                    <h2 className='font-bold text-xl mb-3'>Available Notes</h2>
                    <Link
                        to='/create'
                        className='bg-blue-600 text-white rounded-md p-4 mb-3 block'
                    >
                        Add Note
                    </Link>
                </header>

                <div className='w-full'>
                    <Link
                        to={`/notes/1`}
                        className='bg-[#EEE7DA] rounded-md p-4 mb-3 block'
                    >
                        <h3 className='font-bold text-lg mb-2'>Hey</h3>
                        <p className='text-sm'>Created on 6 January, 2024</p>
                    </Link>
                    <Link
                        to={`/notes/2`}
                        className='bg-[#EEE7DA] rounded-md p-4 mb-3 block'
                    >
                        <h3 className='font-bold text-lg mb-2'>Hello</h3>
                        <p className='text-sm'>Created on 6 January, 2024</p>
                    </Link>
                </div>
            </main>
        </div>
    );
}

The Content Component

The Content component displays the content of each note and allows users to update and delete notes from the application.

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

export default function Content() {
    const { slug } = useParams();

    return (
        <div className='w-full'>
            <nav className='px-8 h-[10vh] bg-[#304D30] w-full flex items-center justify-between'>
                <Link to='/dashboard' className='text-2xl font-bold text-[#EEE7DA]'>
                    Note-Keep
                </Link>
                <div>
                    <button
                        onClick={handleDelete}
                        className='p-3 bg-blue-600 text-white px-4 rounded-md mr-3'
                    >
                        Update
                    </button>
                    <button
                        className='p-3 bg-red-400 text-white px-4 rounded-md'
                        onClick={handleUpdate}
                    >
                        Delete Note
                    </button>
                </div>
            </nav>

            <main className='p-8 w-full '>
                <h2 className='font-bold text-2xl'>Hello World</h2>
                <p className='text-sm mb-3 opacity-40 italic'>
                    Created on 6 January, 2024
                </p>

                <div className='mt-8 opacity-60'>
                    In the quiet corners of a bustling city, where the hum of daily life
                    echoes through narrow alleys, there exists a hidden world of stories
                    waiting to be uncovered. It's a place where time seems to dance at its
                    own rhythm, where the mundane and the extraordinary converge in a
                    delicate balance. Amidst the labyrinthine streets, a mysterious
                    bookstore with a faded sign beckons those who dare to enter.
                </div>
            </main>
        </div>
    );
}

React-Supabase-Development

The CreateContent Component

The CreateContent component enables users to create notes within the application.

It renders a form that accepts the note title and content from the user and saves it to the database.

import { useState } from "react";
import { MdCancel } from "react-icons/md";
import { useNavigate } from "react-router-dom";

export default function CreateContent() {
    const [title, setTitle] = useState("");
    const [content, setContent] = useState("");
    const navigate = useNavigate();

    const handleCancel = () => navigate("/dashboard");

    const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        console.log({ title, content });
    };

    return (
        <div className='w-full p-8'>
            <header className='flex justify-between items-center mb-4'>
                <h2 className='text-3xl font-bold text-[#304D30]'>Create Content</h2>
                <MdCancel
                    className='text-5xl text-red-500 cursor-pointer'
                    onClick={handleCancel}
                />
            </header>

            <form className='w-full flex flex-col' onSubmit={handleSubmit}>
                <label htmlFor='title'>Title</label>
                <input
                    type='text'
                    name='title'
                    className='border-2 p-4 rounded-md text-lg mb-5'
                    value={title}
                    onChange={(e) => setTitle(e.target.value)}
                />

                <label htmlFor='content'>Content</label>
                <textarea
                    name='content'
                    rows={10}
                    className='border-2 p-4 rounded-md text-lg mb-3'
                    value={content}
                    onChange={(e) => setContent(e.target.value)}
                />

                <button className='px-6 py-3 bg-blue-500 text-white w-[200px] rounded-md'>
                    SAVE
                </button>
            </form>
        </div>
    );
}

How to Authenticate Users with Supabase

Supabase provides various forms of authentication. However, in this section, you’ll learn how to authenticate users with Supabase. 

I’ll walk you through how to add email and password authentication to your application and ensure that only authenticated users can visit some specific pages within your application.

Signing up New Users

To log users into the application, you need to create an account using the Supabase client library.

Therefore, import Supabase from the `supabaseClient.ts` file into the Register component.

//👇🏻 supabaseClient.ts file
import { createClient } from "@supabase/supabase-js";

const supaseURL = import.meta.env.VITE_SUPABASE_URL;
const AnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;

//👇🏻 required export
export const supabase = createClient(supaseURL, AnonKey);

Update the handleSubmit function within the Register component to create an account for the user using the credentials provided when the user submits the form.

import { supabase } from "../../supabaseClient";

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    handleSignUp();
};

const handleSignUp = async () => {
    try {
        const { error } = await supabase.auth.signUp({
            email,
            password,
            options: {
                data: {
                    fullName,
                },
            },
        });
        if (error) throw error;
        alert("Account created successfully");
        navigate("/login");
    } catch (error) {
        alert(`Authentication Error - Invalid Credentials`);
    }
};

The code snippet above accepts the user’s email, full name, and password from the form and creates an account using the user’s credentials. If the account is created successfully, the application redirects the user to the login page. Otherwise, Supabase returns an error.

Signing in Existing Users

To log users into the application, execute the code snippet below when the user submits the login form.

import { supabase } from "../../supabaseClient";

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    handleSignIn();
};

const handleSignIn = async () => {
    try {
        const { data, error } = await supabase.auth.signInWithPassword({
            email,
            password,
        });
        if (error) throw error;
        sessionStorage.setItem("token", JSON.stringify(data));
        navigate("/dashboard");
    } catch (error) {
        alert(`Authentication Error - Invalid Credentials`);
    }
};

The code snippet above verifies the user’s email and password to ensure that such a user exists before allowing them access to the application. It also stores the user’s data in the browser’s session storage, enabling us to differentiate authenticated users from unauthenticated ones.

Logging Users Out

Supabase also provides a signOut function that enables users to log out of the application. The code snippet below logs the user out of the application and deletes the saved data from the browser’s session storage before redirecting them to the home page.

import { supabase } from "../../supabaseClient";

const handleLogout = async () => {
    const { error } = await supabase.auth.signOut();
    sessionStorage.removeItem("token");
    if (error) return alert("Encountered an error ⚡️");
    navigate("/");
};

Protecting Pages from Unauthenticated Users

The Dashboard page, the route for viewing existing notes and creating new notes should be protected within the application and only available to authenticated users. 

To do this, we can use the data stored within the browser’s session storage and ensure that only users with such data can access the protected pages within the application.

Therefore, add a useEffect hook that ensures that the data exists within the browser’s session storage before they can view the page’s information.

export default function Component() {
    const [token, setToken] = useState(false);

    useEffect(() => {
        const fetchSession = () => {
            const session = sessionStorage.getItem("token");
            if (session) {
                setToken(true);
            }
        };
        fetchSession();
    }, []);

    if (!token) return <NotAuthenticated />;

    return <div>{/** -- page content -- */}</div>;
}

The code snippet above ensures the token (user’s data) exists within the browser’s session storage before displaying the page’s content. Otherwise, it returns an error page showing that the user is not authenticated.

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

In the upcoming sections, you’ll learn how to fetch, delete, create, and update data from Supabase.


How to Perform CRUD Operations with Supabase

Most applications allow users to perform CRUD (Create, Read, Update, and Delete) operations within the database.

In this section, I’ll walk you through how to interact with Supabase to perform these operations.

However, before you can start interacting with the Supabase database, you need to set up a table within the database that contains the column attributes for the data.

Click Table Editor from the sidebar menu within your Supabase project and create a table called notes.

Supabase-Table-Editor

Add an id, created_at, title, content, user, and slug columns to the table, as shown below:

Supabse-Table-view

Next, we can start adding data to the table and interacting with them.

Saving Data to Supabase

To save notes to the database, you need to write a function that selects the table and inserts the newly created note database, as shown below:

//👇🏻 saves note to the database
const saveNote = async () => {
    try {
        const { error } = await supabase
            .from("notes")
            .insert({ title, content, slug: titleToSlug(title), user: userID })
            .single();
        if (error) throw error;
        navigate("/dashboard");
    } catch (err) {
        console.error(err);
    }
};

//👇🏻 creates a slug from the title
export const titleToSlug = (title: string): string => {
    const slug = title.toLowerCase().replace(/\s+/g, "-");
    const cleanSlug = slug.replace(/[^a-zA-Z0-9-]/g, "");
    return cleanSlug + `${Math.random().toString(36).substring(2, 10)}`;
};

The code snippet above accepts the note’s title and content from the “Create Note” form and saves the data together with the slug and user id to the Supabase database when the form is submitted.

Retrieving Data from Supabase

Within the application, the user’s notes are displayed on the dashboard after authentication. Therefore, you need to fetch all the notes when the user navigates to the dashboard page.

//👇🏻 gets user's notes after authentication
useEffect(() => {
    const fetchSession = () => {
        const session = sessionStorage.getItem("token");
        if (session) {
            setToken(true);
            const userDetails = JSON.parse(session);
            fetchNotes(userDetails.user.id);
        }
    };
    fetchSession();
}, []);

//👇🏻 fetch user's notes from  Supabase
async function fetchNotes(id: string) {
    try {
        const { data, error } = await supabase
            .from("notes")
            .select()
            .eq("user", id);
        if (error) throw error;
        setNotes(data);
    } catch (error) {
        alert("Error fetching notes");
    }
}

The code snippet above gets the user’s ID from the data returned after authentication.

Then, all the notes related to the user are retrieved from Supabase and displayed on the web page.

Getting a Single Data from Supabase

You can get all the data related to a particular note via its slug.

Each note within the application has a slug attribute that enables us to view a particular note within the application.

A slug is a user-friendly string derived from a title. Unlike number IDs like 1, 2, and 3, slugs are URL-valid strings that are easy to memorize.

The code snippet below gets a specific note from the database via the slug and displays its content to the user.

async function fetchData() {
    try {
        const { data, error } = await supabase
            .from("notes")
            .select()
            .eq("slug", slug);
        if (error) throw error;
        setData(data);
    } catch (err) {
        console.error(err);
    }
}

useEffect(() => {
    const fetchSession = () => {
        const session = sessionStorage.getItem("token");
        if (session) {
            setToken(true);
            fetchData();
        }
    };
    fetchSession();
}, []);

Updating Data within Supabase

The code snippet below updates the title, content, and slug attributes of a selected item within the table. The noteTitle and noteContent are the updated versions of the note item, and the noteID is the ID of the updated note.

const updateNoteData = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    try {
        const { error } = await supabase
            .from("notes")
            .update({
                title: noteTitle,
                content: noteContent,
                slug: titleToSlug(noteTitle),
            })
            .eq("id", noteID);
        if (error) throw error;
        alert("Updated successfully!🎉");
        navigate("/dashboard");
    } catch (err) {
        console.error(err);
    }
};

Deleting Data from Supabase

To delete a particular note from the table, you can select the item via its ID, and Supabase permanently deletes it.

const handleDelete = async () => {
    try {
        const { error } = await supabase.from("notes").delete().eq("id", noteID);
        if (error) throw error;
        navigate("/dashboard");
    } catch (err) {
        console.error(err);
    }
};

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

Conclusion

So far, you’ve learnt what Supabase is, why you should use it, and how to leverage the various features, e.g. authentication and database, provided by Supabase to build scalable and secured full-stack applications.

Supabase is an amazing software that enables developers to build full-stack and complex applications within a few minutes.

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

You can also explore other features, such as file storage, vector database, and edge functions to build modern software applications. Additionally, Supabase 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 about ReactJS and JavaScript, check out my recent articles.

Leave a Comment

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