Leveraging Supabase in React JS: Building a To-Do App with Sign-Up

Leveraging Supabase in React JS: Building a To-Do App with Sign-Up

Building a To-Do App with Supabase and React JS: A Step-by-Step Guide

·

9 min read

In the fast-paced world of web development, developers are always on the lookout for tools that simplify workflows, reduce complexity, and accelerate project delivery. One such tool that has emerged as a game-changer is Supabase, an open-source backend-as-a-service (BaaS) platform often dubbed the "Firebase alternative." With its powerful features—including a PostgreSQL database, authentication, real-time subscriptions, and storage—Supabase empowers developers to build robust applications without the overhead of managing a traditional backend.

In this article, we’ll explore how to integrate Supabase into a React JS application by building a practical to-do app with sign-up functionality. We’ll walk through the setup process, implement user authentication, and create CRUD (Create, Read, Update, Delete) operations for managing to-do items. Additionally, we’ll discuss why Supabase is particularly helpful for small apps, how it adds value to development, and what sets it apart from other solutions. Whether you’re a beginner or an experienced developer, this guide will provide you with a solid foundation to leverage Supabase in your next project.


What is Supabase?

Before diving into the code, let’s understand what Supabase brings to the table. Supabase is an open-source platform that provides a suite of tools to streamline backend development. Built on top of PostgreSQL, a highly reliable and feature-rich relational database, Supabase offers:

  • Database Management: A fully managed PostgreSQL instance with a user-friendly dashboard.

  • Authentication: Built-in support for email/password, OAuth, and magic links.

  • Real-Time Subscriptions: Live data updates across clients without additional setup.

  • Storage: File storage for images, documents, and more.

  • APIs: Auto-generated REST and GraphQL APIs for your database.

Unlike proprietary platforms, Supabase’s open-source nature allows developers to customize it, self-host it, or contribute to its ecosystem. Its seamless integration with modern JavaScript frameworks like React JS makes it an appealing choice for frontend developers looking to minimize backend complexity.


Why Supabase is Perfect for Small Apps

Small applications—such as personal projects, prototypes, or startup MVPs—often face constraints like limited budgets, tight timelines, and small teams. Supabase shines in these scenarios for several reasons:

  1. Quick and Easy Setup: Supabase eliminates the need for complex server setup, allowing developers to spin up a backend in minutes.

  2. Generous Free Tier: The free plan includes ample resources (e.g., 500 MB database, 1 GB file storage), making it cost-effective for small-scale projects.

  3. Reduced Backend Overhead: With authentication, database, and real-time features out of the box, developers can focus on the frontend rather than building and maintaining a custom backend.

  4. Scalability: While tailored for small apps, Supabase’s infrastructure scales effortlessly as your project grows.

  5. Developer-Friendly: Its JavaScript client library integrates smoothly with React, and the SQL-based PostgreSQL database is familiar to many developers.

For a small to-do app, these benefits translate to faster development, lower costs, and a smoother path from idea to deployment.


Getting Started: Setting Up Supabase with React JS

Let’s build a to-do app with sign-up functionality to see Supabase in action. Here’s what you’ll need:

  • A Supabase account and a new project created via the Supabase dashboard.

  • A React JS project (e.g., created with npx create-react-app todo-app).

  • Node.js and npm installed on your machine.

Step 1: Install the Supabase JavaScript Client

First, install the Supabase client library in your React project:

npm install @supabase/supabase-js

Step 2: Initialize the Supabase Client

In your project, create a file named supabaseClient.js to set up the Supabase client. You’ll need your project’s URL and anonymous key, which you can find in the Supabase dashboard under Settings > API.

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

const supabaseUrl = 'YOUR_SUPABASE_URL'; // e.g., https://xyz.supabase.co
const supabaseAnonKey = 'YOUR_SUPABASE_ANON_KEY';

export const supabase = createClient(supabaseUrl, supabaseAnonKey);

This initializes a client instance that we’ll use to interact with Supabase services.


Implementing User Authentication

A to-do app is more useful when tied to a user account, so let’s implement sign-up and login functionality using Supabase’s authentication system.

Step 3: Configure Authentication in Supabase

In the Supabase dashboard, navigate to the Authentication section. Enable the email provider and configure settings like email confirmation (optional for this example). Supabase handles user management, including password hashing and session tokens, behind the scenes.

Step 4: Sign-Up Form in React

Create a SignUp.js component to allow users to register:

import React, { useState } from 'react';
import { supabase } from './supabaseClient';

function SignUp() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [message, setMessage] = useState('');

  const handleSignUp = async () => {
    const { data, error } = await supabase.auth.signUp({ email, password });
    if (error) setMessage(`Error: ${error.message}`);
    else setMessage('Sign-up successful! Check your email to confirm.');
  };

  return (
    <div>
      <h2>Sign Up</h2>
      <input
        type="email"
        placeholder="Email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <input
        type="password"
        placeholder="Password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <button onClick={handleSignUp}>Sign Up</button>
      {message && <p>{message}</p>}
    </div>
  );
}

export default SignUp;

This form collects the user’s email and password, then uses supabase.auth.signUp() to register them. If email confirmation is enabled, users will need to verify their email before logging in.

Step 5: Login Form in React

Next, create a Login.js component for users to log in:

import React, { useState } from 'react';
import { supabase } from './supabaseClient';

function Login() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [message, setMessage] = useState('');

  const handleLogin = async () => {
    const { data, error } = await supabase.auth.signInWithPassword({ email, password });
    if (error) setMessage(`Error: ${error.message}`);
    else setMessage('Logged in successfully!');
  };

  return (
    <div>
      <h2>Login</h2>
      <input
        type="email"
        placeholder="Email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <input
        type="password"
        placeholder="Password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <button onClick={handleLogin}>Login</button>
      {message && <p>{message}</p>}
    </div>
  );
}

export default Login;

The supabase.auth.signInWithPassword() method authenticates the user and starts a session.


Building the To-Do App with CRUD Operations

With authentication in place, let’s create a todos table in Supabase and implement CRUD operations to manage to-do items.

Step 6: Create the Todos Table

In the Supabase dashboard, go to the SQL Editor and run this query to create a todos table:

CREATE TABLE todos (
  id SERIAL PRIMARY KEY,
  user_id UUID REFERENCES auth.users(id),
  task TEXT NOT NULL,
  is_complete BOOLEAN DEFAULT FALSE,
  inserted_at TIMESTAMP DEFAULT NOW()
);

This table links each to-do item to a user via user_id, ensuring users only see their own tasks.

Step 7: CRUD Operations in React

Create a TodoApp.js component to handle the to-do functionality. Here’s a complete example:

import React, { useState, useEffect } from 'react';
import { supabase } from './supabaseClient';

function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [newTask, setNewTask] = useState('');
  const [loading, setLoading] = useState(false);

  // Fetch todos when component mounts
  useEffect(() => {
    fetchTodos();
  }, []);

  const fetchTodos = async () => {
    setLoading(true);
    const { data, error } = await supabase
      .from('todos')
      .select('*')
      .eq('user_id', supabase.auth.getUser().id);
    if (error) console.error('Error fetching todos:', error.message);
    else setTodos(data);
    setLoading(false);
  };

  const addTodo = async () => {
    const { data, error } = await supabase
      .from('todos')
      .insert([{ task: newTask, user_id: supabase.auth.getUser().id }])
      .select();
    if (error) console.error('Error adding todo:', error.message);
    else setTodos([...todos, data[0]]);
    setNewTask('');
  };

  const toggleComplete = async (id, is_complete) => {
    const { data, error } = await supabase
      .from('todos')
      .update({ is_complete: !is_complete })
      .eq('id', id)
      .select();
    if (error) console.error('Error updating todo:', error.message);
    else setTodos(todos.map((todo) => (todo.id === id ? data[0] : todo)));
  };

  const deleteTodo = async (id) => {
    const { error } = await supabase.from('todos').delete().eq('id', id);
    if (error) console.error('Error deleting todo:', error.message);
    else setTodos(todos.filter((todo) => todo.id !== id));
  };

  if (!supabase.auth.getUser()) {
    return <p>Please log in to view your todos.</p>;
  }

  return (
    <div>
      <h2>Your To-Do List</h2>
      <input
        type="text"
        value={newTask}
        onChange={(e) => setNewTask(e.target.value)}
        placeholder="Add a new task"
      />
      <button onClick={addTodo} disabled={!newTask}>Add Task</button>
      {loading ? (
        <p>Loading...</p>
      ) : (
        <ul>
          {todos.map((todo) => (
            <li key={todo.id}>
              <input
                type="checkbox"
                checked={todo.is_complete}
                onChange={() => toggleComplete(todo.id, todo.is_complete)}
              />
              {todo.task}
              <button onClick={() => deleteTodo(todo.id)}>Delete</button>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default TodoApp;

Explanation of the Code

  • Fetch Todos: Retrieves the user’s to-do items when the component mounts.

  • Add Todo: Inserts a new task into the todos table, tied to the logged-in user.

  • Toggle Complete: Updates the is_complete status of a task.

  • Delete Todo: Removes a task from the table.

  • User Check: Ensures only authenticated users can access the to-do list.

You can integrate this component into your app with the SignUp and Login components, using React Router or conditional rendering to manage navigation.


How Supabase Enhances Development

Supabase isn’t just a tool—it’s a productivity booster. Here’s how it proves useful:

  • Rapid Prototyping: With pre-built features, you can go from idea to working app in hours.

  • Real-Time Features: The built-in real-time subscriptions (e.g., via supabase.from('todos').subscribe()) make it easy to keep data in sync across users.

  • Simplified Authentication: No need to build login systems from scratch—Supabase handles it securely.

  • SQL Power: PostgreSQL’s capabilities allow for complex queries and relationships, far beyond basic key-value stores.

For our to-do app, Supabase reduced the need for a custom server, API endpoints, or authentication logic, letting us focus on the user experience.


Why Supabase Stands Out

Compared to alternatives like Firebase, Supabase offers distinct advantages:

  1. Open-Source Freedom: You can self-host Supabase or extend its functionality, unlike Firebase’s closed ecosystem.

  2. PostgreSQL vs. NoSQL: Firebase uses Firestore (NoSQL), while Supabase’s PostgreSQL supports relational data and advanced SQL features.

  3. Transparent Pricing: Supabase’s pricing is straightforward, with no hidden costs, and its free tier is generous.

  4. Real-Time by Default: Real-time updates are seamless with Supabase, requiring less setup than Firebase’s equivalents.

  5. Community-Driven: The open-source community ensures continuous improvements and support.

For small apps, these factors translate to greater control, flexibility, and cost savings—key considerations when resources are limited.


Best Practices and Pitfalls

To get the most out of Supabase in React:

  • Secure Your App: Use Row-Level Security (RLS) in Supabase to restrict data access (e.g., user_id filtering in our example).

  • Handle Errors: Always provide user feedback for authentication or database errors.

  • Manage State: Use React’s state management (or libraries like Redux) to sync UI with Supabase data.

  • Avoid Overfetching: Fetch only the data you need to optimize performance.

A common pitfall is neglecting RLS, which could expose data to unauthorized users. Always enable and test security policies in the Supabase dashboard.


Conclusion

Supabase is a remarkable tool that bridges the gap between frontend development and backend complexity. By integrating it with React JS, we built a fully functional to-do app with sign-up and CRUD operations in a fraction of the time it would take with a traditional backend. Its ease of use, cost-effectiveness, and powerful features make it an ideal choice for small apps, while its scalability ensures it can grow with your project.

Whether you’re prototyping a startup idea or building a personal project, Supabase empowers you to create efficiently and effectively. I encourage you to explore its capabilities, experiment with the to-do app we’ve built, and discover how it can elevate your development workflow. Happy coding!