Mahbub Khandakar

Beginner's Guide: Connect React with Supabase 🚀

If you already know basic React (components, useState, useEffect), this guide will show you how to connect your React app to Supabase — an open-source Firebase alternative — and build a simple To-Do app with full CRUD (Create, Read, Update, Delete).

Let's go step by step. No prior Supabase knowledge needed.


What is Supabase?

Supabase gives you a Postgres database, authentication, and instant APIs — without writing any backend code. Think of it as a backend-as-a-service.

For this guide, we'll just use the database + auto-generated API part.


Step 1: Create a Supabase Project

  1. Go to supabase.com and sign up (GitHub login is fastest).
  2. Click New Project.
  3. Fill in:
    • Name: todo-app (anything you like)
    • Database Password: save this somewhere safe
    • Region: pick the closest one to you
  4. Click Create new project and wait ~1-2 minutes while Supabase sets everything up.

Step 2: Create the todos Table

  1. In your Supabase project dashboard, go to the Table Editor (left sidebar).
  2. Click New Table.
  3. Name it todos.
  4. Add these columns (in addition to the default id and created_at):
Column Name Type Default
task text
is_complete bool false
  1. Click Save.

💡 Tip: You can also do this via the SQL Editor by running:

create table todos (
  id bigint generated by default as identity primary key,
  task text not null,
  is_complete boolean default false,
  created_at timestamp with time zone default now()
);

Turn off Row Level Security (for learning purposes only)

Go to Authentication > Policies (or Table Editor > todos > RLS), and disable RLS for now so your students can read/write freely without setting up auth.

⚠️ Important for your session: Tell your juniors this is only for a demo/learning project. In a real production app, RLS should always be enabled with proper policies.


Step 3: Get Your API Keys

  1. Go to Project Settings > API.
  2. Copy two things:
    • Project URL (looks like https://xxxxx.supabase.co)
    • anon public key (a long string)

You'll need both in your React app.


Step 4: Set Up the React Project

If they already have a React project (Vite recommended), just install the Supabase client:

npm install @supabase/supabase-js

Enter fullscreen mode Exit fullscreen mode

If starting fresh:

npm create vite@latest todo-app -- --template react
cd todo-app
npm install
npm install @supabase/supabase-js

Enter fullscreen mode Exit fullscreen mode


Step 5: Create a Supabase Client File

Create a file src/supabaseClient.js:

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

const supabaseUrl = 'YOUR_PROJECT_URL'
const supabaseAnonKey = 'YOUR_ANON_KEY'

export const supabase = createClient(supabaseUrl, supabaseAnonKey)

Enter fullscreen mode Exit fullscreen mode

💡 Best practice: use environment variables instead of hardcoding keys.
Create a .env file:

VITE_SUPABASE_URL=your_project_url
VITE_SUPABASE_ANON_KEY=your_anon_key

Then use:

const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY

(For a beginner live demo, hardcoding is fine to keep things simple — just mention the .env best practice.)


Step 6: Build the To-Do Component

Replace src/App.jsx with this:

import { useState, useEffect } from 'react'
import { supabase } from './supabaseClient'
import './App.css'

function App() {
  const [todos, setTodos] = useState([])
  const [newTask, setNewTask] = useState('')
  const [loading, setLoading] = useState(true)

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

  async function fetchTodos() {
    setLoading(true)
    const { data, error } = await supabase
      .from('todos')
      .select('*')
      .order('created_at', { ascending: false })

    if (error) console.error('Error fetching todos:', error)
    else setTodos(data)
    setLoading(false)
  }

  async function addTodo(e) {
    e.preventDefault()
    if (!newTask.trim()) return

    const { data, error } = await supabase
      .from('todos')
      .insert([{ task: newTask, is_complete: false }])
      .select()

    if (error) console.error('Error adding todo:', error)
    else {
      setTodos([data[0], ...todos])
      setNewTask('')
    }
  }

  async function toggleComplete(id, currentStatus) {
    const { error } = await supabase
      .from('todos')
      .update({ is_complete: !currentStatus })
      .eq('id', id)

    if (error) console.error('Error updating todo:', error)
    else {
      setTodos(todos.map(t => t.id === id ? { ...t, is_complete: !currentStatus } : t))
    }
  }

  async function deleteTodo(id) {
    const { error } = await supabase
      .from('todos')
      .delete()
      .eq('id', id)

    if (error) console.error('Error deleting todo:', error)
    else setTodos(todos.filter(t => t.id !== id))
  }

  return (
    <div style={{ maxWidth: 500, margin: '40px auto', fontFamily: 'sans-serif' }}>
      <h1>📝 Supabase To-Do App</h1>

      <form onSubmit={addTodo} style={{ display: 'flex', gap: 8, marginBottom: 20 }}>
        <input
          type="text"
          value={newTask}
          onChange={(e) => setNewTask(e.target.value)}
          placeholder="Add a new task..."
          style={{ flex: 1, padding: 8 }}
        />
        <button type="submit" style={{ padding: '8px 16px' }}>Add</button>
      </form>

      {loading ? (
        <p>Loading...</p>
      ) : (
        <ul style={{ listStyle: 'none', padding: 0 }}>
          {todos.map((todo) => (
            <li
              key={todo.id}
              style={{
                display: 'flex',
                alignItems: 'center',
                gap: 10,
                padding: 10,
                borderBottom: '1px solid #eee',
              }}
            >
              <input
                type="checkbox"
                checked={todo.is_complete}
                onChange={() => toggleComplete(todo.id, todo.is_complete)}
              />
              <span
                style={{
                  flex: 1,
                  textDecoration: todo.is_complete ? 'line-through' : 'none',
                  color: todo.is_complete ? '#999' : '#000',
                }}
              >
                {todo.task}
              </span>
              <button onClick={() => deleteTodo(todo.id)} style={{ color: 'red' }}>
                Delete
              </button>
            </li>
          ))}
        </ul>
      )}
    </div>
  )
}

export default App

Enter fullscreen mode Exit fullscreen mode


Step 7: Run It

npm run dev

Enter fullscreen mode Exit fullscreen mode

Open the local URL shown in the terminal — you should be able to add, complete, and delete tasks, and see them saved in your Supabase Table Editor in real time.