Let’s Build an App with React + Supabase (Firebase Alternative)

July 7, 2021, 8:55 p.m.
React · 10 min read
Let’s Build an App with React + Supabase (Firebase Alternative)

What is Supabase?

Supabase is an open-source alternative to Google's Firebase. It's currently free to use, supporting PostgreSQL databases and authentication tools. 

It relies on existing open-source projects and a serverless solution will be released soon. 

 

React + Supabase Tutorial

Create a Supabase account

Create Supabase account

First, you'll need to create a Supabase account by connecting to your Github.

 

Create a new project

Supabase create new project

Next, click the "New Project" button in the dashboard. 

 

Supabase create new project info

Then enter the name, database password and select your region.

Now you'll wait for your database to launch. It will take around 2 mins. 

 

Set up the database schema

Supabase SQL tab

1. Click on the SQL tab on the dashboard navigation.

Supabase SQL User Management Starter

2. Scroll down to the "Quick start" starters section and click on the "User Management Starter" script.

Supabase User Management Starter run

3. Click "RUN" on the right side of the lower right side of the screen. When complete, the Results will read "Success. No rows returned".

You just created a database table for public profiles. 

 

Insert data using the API

Now it's time to insert some data using the Supabase auto-generated API. 

1. Click on the Settings tab on the dashboard navigation.

2. Click on "API" in the menu.

3. Find the following:

  • API URL under "Config"
  • anon key under "API Keys"
  • service_role key under "API Keys"

These API settings will be needed in a minute to connect the React app to the Supabase db.

 

Create a React app

Windows Command Prompt

C:\Users\Owner\desktop\react> npx create-react-app supareact

Create a React app named supareact using Facebook's create-react-app. See the beginner's guide if you're new to React.

 

Install supabase-js

Windows Command Prompt

C:\Users\Owner\desktop\react> cd supareact
C:\Users\Owner\desktop\react\supareact> npm install @supabase/supabase-js

After installation, enter into the newly created project directory and install the supabase-js dependency. 

 

Add the Supabase API credentials as environment variables

supareact > (New File) .env

REACT_APP_SUPABASE_URL=YOUR_SUPABASE_URL
REACT_APP_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY

Create a new file called .env in the project directory supareact, and add the API URL and the anon key.

 

Initialize the Supabase client

supareact > src > (New File) supabaseClient.js

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

const supabaseUrl = process.env.REACT_APP_SUPABASE_URL
const supabaseAnonKey = process.env.REACT_APP_SUPABASE_ANON_KEY

export const supabase = createClient(supabaseUrl, supabaseAnonKey)

Next, we need a helper file that initializes the Supabase client. This is where the environment variables are called to connect to Supabase. 

The supabase constant will be called throughout the files added. 

 

Supabase Row Level Security

These environment variables are exposed on the browser, but Supabase's Row Level Security is enabled by default, allowing public profiles to be viewed by everyone but only users can update and insert their own profiles. 

 

Update the CSS file

supareact > src > index.css

html,
body {
  --custom-font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu,
    Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
  --custom-bg-color: #101010;
  --custom-panel-color: #222;
  --custom-box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.8);
  --custom-color: #fff;
  --custom-color-brand: #24b47e;
  --custom-color-secondary: #666;
  --custom-border: 1px solid #333;
  --custom-border-radius: 5px;
  --custom-spacing: 5px;

  padding: 0;
  margin: 0;
  font-family: var(--custom-font-family);
  background-color: var(--custom-bg-color);
}

* {
  color: var(--custom-color);
  font-family: var(--custom-font-family);
  box-sizing: border-box;
}

html,
body,
#__next {
  height: 100vh;
  width: 100vw;
  overflow-x: hidden;
}

/* Grid */

.container {
  width: 90%;
  margin-left: auto;
  margin-right: auto;
}
.row {
  position: relative;
  width: 100%;
}
.row [class^='col'] {
  float: left;
  margin: 0.5rem 2%;
  min-height: 0.125rem;
}
.col-1,
.col-2,
.col-3,
.col-4,
.col-5,
.col-6,
.col-7,
.col-8,
.col-9,
.col-10,
.col-11,
.col-12 {
  width: 96%;
}
.col-1-sm {
  width: 4.33%;
}
.col-2-sm {
  width: 12.66%;
}
.col-3-sm {
  width: 21%;
}
.col-4-sm {
  width: 29.33%;
}
.col-5-sm {
  width: 37.66%;
}
.col-6-sm {
  width: 46%;
}
.col-7-sm {
  width: 54.33%;
}
.col-8-sm {
  width: 62.66%;
}
.col-9-sm {
  width: 71%;
}
.col-10-sm {
  width: 79.33%;
}
.col-11-sm {
  width: 87.66%;
}
.col-12-sm {
  width: 96%;
}
.row::after {
  content: '';
  display: table;
  clear: both;
}
.hidden-sm {
  display: none;
}

@media only screen and (min-width: 33.75em) {
  /* 540px */
  .container {
    width: 80%;
  }
}

@media only screen and (min-width: 45em) {
  /* 720px */
  .col-1 {
    width: 4.33%;
  }
  .col-2 {
    width: 12.66%;
  }
  .col-3 {
    width: 21%;
  }
  .col-4 {
    width: 29.33%;
  }
  .col-5 {
    width: 37.66%;
  }
  .col-6 {
    width: 46%;
  }
  .col-7 {
    width: 54.33%;
  }
  .col-8 {
    width: 62.66%;
  }
  .col-9 {
    width: 71%;
  }
  .col-10 {
    width: 79.33%;
  }
  .col-11 {
    width: 87.66%;
  }
  .col-12 {
    width: 96%;
  }
  .hidden-sm {
    display: block;
  }
}

@media only screen and (min-width: 60em) {
  /* 960px */
  .container {
    width: 75%;
    max-width: 60rem;
  }
}

/* Forms */

label {
  display: block;
  margin: 5px 0;
  color: var(--custom-color-secondary);
  font-size: 0.8rem;
  text-transform: uppercase;
}

input {
  width: 100%;
  border-radius: 5px;
  border: var(--custom-border);
  padding: 8px;
  font-size: 0.9rem;
  background-color: var(--custom-bg-color);
  color: var(--custom-color);
}

input[disabled] {
  color: var(--custom-color-secondary);
}

/* Utils */

.block {
  display: block;
  width: 100%;
}
.inline-block {
  display: inline-block;
  width: 100%;
}
.flex {
  display: flex;
}
.flex.column {
  flex-direction: column;
}
.flex.row {
  flex-direction: row;
}
.flex.flex-1 {
  flex: 1 1 0;
}
.flex-end {
  justify-content: flex-end;
}
.flex-center {
  justify-content: center;
}
.items-center {
  align-items: center;
}
.text-sm {
  font-size: 0.8rem;
  font-weight: 300;
}
.text-right {
  text-align: right;
}
.font-light {
  font-weight: 300;
}
.opacity-half {
  opacity: 50%;
}

/* Button */

button,
.button {
  color: var(--custom-color);
  border: var(--custom-border);
  background-color: var(--custom-bg-color);
  display: inline-block;
  text-align: center;
  border-radius: var(--custom-border-radius);
  padding: 0.5rem 1rem;
  cursor: pointer;
  text-align: center;
  font-size: 0.9rem;
  text-transform: uppercase;
}

button.primary,
.button.primary {
  background-color: var(--custom-color-brand);
  border: 1px solid var(--custom-color-brand);
}

/* Widgets */

.card {
  width: 100%;
  display: block;
  border: var(--custom-border);
  border-radius: var(--custom-border-radius);
  padding: var(--custom-spacing);
}

.avatar {
  border-radius: var(--custom-border-radius);
  overflow: hidden;
  max-width: 100%;
}
.avatar.image {
  object-fit: cover;
}
.avatar.no-image {
  background-color: #333;
  border: 1px solid rgb(200, 200, 200);
  border-radius: 5px;
}

.footer {
  position: absolute;
  max-width: 100%;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  flex-flow: row;
  border-top: var(--custom-border);
  background-color: var(--custom-bg-color);
}
.footer div {
  padding: var(--custom-spacing);
  display: flex;
  align-items: center;
  width: 100%;
}
.footer div > img {
  height: 20px;
  margin-left: 10px;
}
.footer > div:first-child {
  display: none;
}
.footer > div:nth-child(2) {
  justify-content: left;
}

@media only screen and (min-width: 60em) {
  /* 960px */
  .footer > div:first-child {
    display: flex;
  }
  .footer > div:nth-child(2) {
    justify-content: center;
  }
}

@keyframes spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

.mainHeader {
  width: 100%;
  font-size: 1.3rem;
  margin-bottom: 20px;
}

.avatarPlaceholder {
  border: var(--custom-border);
  border-radius: var(--custom-border-radius);
  width: 35px;
  height: 35px;
  background-color: rgba(255, 255, 255, 0.2);
  display: flex;
  align-items: center;
  justify-content: center;
}

.form-widget {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.form-widget > .button {
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background-color: #444444;
  text-transform: none !important;
  transition: all 0.2s ease;
}

.form-widget .button:hover {
  background-color: #2a2a2a;
}

.form-widget .button > .loader {
  width: 17px;
  animation: spin 1s linear infinite;
  filter: invert(1);
}

Before adding any components, update the CSS file index.css with the CSS declarations provided by Supabase. Delete the default CSS declarations initially in the file. 

 

Create a login component

supareact > src > (New File) Auth.js

import { useState } from 'react'
import { supabase } from './supabaseClient'

export default function Auth() {
  const [loading, setLoading] = useState(false)
  const [email, setEmail] = useState('')

  const handleLogin = async (email) => {
    try {
      setLoading(true)
      const { error } = await supabase.auth.signIn({ email })
      if (error) throw error
      alert('Check your email for the login link!')
    } catch (error) {
      alert(error.error_description || error.message)
    } finally {
      setLoading(false)
    }
  }

  return (
    <div className="row flex flex-center">
      <div className="col-6 form-widget">
        <h1 className="header">Supabase + React</h1>
        <p className="description">Sign in via magic link with your email below</p>
        <div>
          <input
            className="inputField"
            type="email"
            placeholder="Your email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          />
        </div>
        <div>
          <button
            onClick={(e) => {
              e.preventDefault()
              handleLogin(email)
            }}
            className={'button block'}
            disabled={loading}
          >
            {loading ? <span>Loading</span> : <span>Send magic link</span>}
          </button>
        </div>
      </div>
    </div>
  )
}

Now let's add a functional component that handles user registration and login. 

Import useState and supabase at the top of the file. 

Remember that useState is a React Hook used to add a React state to a functional component. useState returns the current state and the function that updates the state. 

There are two states set in the Auth component: loading and email

The email state is the input field's value while setEmail() is called on the onChange event to update the email state. By default, the email state is set as an empty string. 

The loading state is called in a conditional (ternary) operator, a shortcut for a JavaScript if statement. 

The statement reads if loading is true, show "Loading", else show "Send magic link" button.

The handleLogin arrow function takes in the email state provided and sends a magic link that looks like this: /verify?type=magiclink&token=hvoiwefbfuiuAYGgfrerg to the email entered on sign up/in. A browser alert appears when the magic link is sent to notify the user. 

Magic link is a way for users to authenticate without a password. 

 

Create an account component

supareact > src > (New File) Account.js

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

export default function Account({ session }) {
  const [loading, setLoading] = useState(true)
  const [username, setUsername] = useState(null)
  const [website, setWebsite] = useState(null)
  const [avatar_url, setAvatarUrl] = useState(null)

  useEffect(() => {
    getProfile()
  }, [session])

  async function getProfile() {
    try {
      setLoading(true)
      const user = supabase.auth.user()

      let { data, error, status } = await supabase
        .from('profiles')
        .select(`username, website, avatar_url`)
        .eq('id', user.id)
        .single()

      if (error && status !== 406) {
        throw error
      }

      if (data) {
        setUsername(data.username)
        setWebsite(data.website)
        setAvatarUrl(data.avatar_url)
      }
    } catch (error) {
      alert(error.message)
    } finally {
      setLoading(false)
    }
  }

  async function updateProfile({ username, website, avatar_url }) {
    try {
      setLoading(true)
      const user = supabase.auth.user()

      const updates = {
        id: user.id,
        username,
        website,
        avatar_url,
        updated_at: new Date(),
      }

      let { error } = await supabase.from('profiles').upsert(updates, {
        returning: 'minimal', // Don't return the value after inserting
      })

      if (error) {
        throw error
      }
    } catch (error) {
      alert(error.message)
    } finally {
      setLoading(false)
    }
  }

  return (
    <div className="form-widget">
      <div>
        <label htmlFor="email">Email</label>
        <input id="email" type="text" value={session.user.email} disabled />
      </div>
      <div>
        <label htmlFor="username">Name</label>
        <input
          id="username"
          type="text"
          value={username || ''}
          onChange={(e) => setUsername(e.target.value)}
        />
      </div>
      <div>
        <label htmlFor="website">Website</label>
        <input
          id="website"
          type="website"
          value={website || ''}
          onChange={(e) => setWebsite(e.target.value)}
        />
      </div>

      <div>
        <button
          className="button block primary"
          onClick={() => updateProfile({ username, website, avatar_url })}
          disabled={loading}
        >
          {loading ? 'Loading ...' : 'Update'}
        </button>
      </div>

      <div>
        <button className="button block" onClick={() => supabase.auth.signOut()}>
          Sign Out
        </button>
      </div>
    </div>
  )
}

Let's also add a component that allows the user to update their account information. 

There are four states in the Account functional component: loading, username, website, and avatar_url

Two asynchronous functions are also in Account:

getProfile() contains a fetch() promise that, when fulfilled, assigns the response to the data, error, and status variables. If the data is provided then the username, website, and avatar are rendered.

updateProfile() sets the constants user and updates. When the function is called, the updates specified by the user are rendered and saved to Supabase.

 

Update the React app with the account and login components

supareact > src > App.js

import './index.css'
import { useState, useEffect } from 'react'
import { supabase } from './supabaseClient'
import Auth from './Auth'
import Account from './Account'

export default function Home() {
  const [session, setSession] = useState(null)

  useEffect(() => {
    setSession(supabase.auth.session())

    supabase.auth.onAuthStateChange((_event, session) => {
      setSession(session)
    })
  }, [])

  return (
    <div className="container" style={{ padding: '50px 0 100px 0' }}>
      {!session ? <Auth /> : <Account key={session.user.id} session={session} />}
    </div>
  )
}

If there is not an authenticated session, then the <Auth /> component will show, else the <Account /> is returned with the Supabase session information. 

 

Start the React app

Windows Command Prompt

C:\Users\Owner\desktop\react\supareact> npm start

It's time to run the development server and see React + Supabase in action.

 

View Supabase + React app in the browser

Supabase + React login/signup page

Here is the login/sign up page. 

 

Supabase + React login/signup page notification

Here is the browser alert. 

 

Supabase + React magic link email

This is the confirmation email sent by Supabase using Magic link. 

 

Supabase login page

And here is the authentication page. 

 

View the profile table in Supabase

Now it's time to see the user in the Supabase dashboard. 

1. Click on the Authentication tab on the dashboard navigation.

2. Click on "Users" in the menu.

Supabase authenticated users

This is where you can see all of the users who signed up.

 

Updating the email templates

Supabase edit email templates

If you want to update the email templates, click on "Templates" in the Authentication tab. 






Post a Comment
Join the community

0 Comments
2
Jaysha
Written By
Jaysha
Hello! I enjoy learning about new CSS frameworks, animation libraries, and SEO.