Authentication with Supabase and React

ruanmartinelli

Ruan Martinelli

Posted on April 7, 2021

Authentication with Supabase and React

Supabase is an open source managed back-end platform. It's a direct alternative to Firebase, which is owned by Google and closed source.

Supabase comes with features such as authentication, object storage, and managed databases. Everything is built on top of open source tools, such as PostgREST and GoTrue. If you want, you can also self-host your own instance of Supabase. As of today, Supabase is in public Beta.

In this tutorial you will learn how to build an a simple React application with authentication using Create React App (CRA). Supabase will serve as the back-end for the authentication part. The application will include sign in, sign up, and a private route than can only be accessed with valid credentials.

If you wanna jump straight to the code, you can check the GitHub repository.

Setting up Supabase

VIsit Supabase's website to create a new account. Click the "Start your project" button and sign in with your GitHub account.

After signing in to the dashboard, hit the green "New Project" button. A modal like this should appear:

1. Create a new project on Supabase

Choose a name for your project and a region close to you. It's required that you set a database password too, but we won't be using any on this tutorial.

It will take a few minutes for the project to be fully created. After it's done, go to Settings > API and copy the URL & Public Anonymous API key. Save the values somewhere, you will need them later on.

2. Copy public credentials

πŸ’‘ Tip: To make it easier to test the sign up flow, you can also disable email confirmations on Authentication > Settings > Disable Email Confirmations.

Setting up the project

Create a new project using Create React App:

npx create-react-app supabase-auth-react
Enter fullscreen mode Exit fullscreen mode
3. Create a new CRA project

I usually do some cleanup on new CRA projects before start developing. Here's how the project structure looks like after moving files around and deleting a few imports:

.
β”œβ”€β”€ package.json
β”œβ”€β”€ .env.local
└── src
    β”œβ”€β”€ components
    β”‚Β Β  β”œβ”€β”€ App.js # Moved from src/
    β”‚Β Β  β”œβ”€β”€ Dashboard.js
    β”‚Β Β  β”œβ”€β”€ Login.js
    β”‚Β Β  β”œβ”€β”€ PrivateRoute.js
    β”‚Β Β  └── Signup.js
    β”œβ”€β”€ contexts
    β”‚Β Β  └── Auth.js
    β”œβ”€β”€ index.js # Already created by CRA
    └── supabase.js
Enter fullscreen mode Exit fullscreen mode
4. Overview of the folder structure after cleaning up CRA files

Feel free to recreate the same file structure. Don't worry about adding any code or trying to make sense of all the components just yet, we will go through everything later.

The src/index.js and src/components/App.js were already created by CRA. Here's how they look after cleaning up:

// src/components/App.js

export function App() {
  return (
    <div>
      <h1>supabase-auth-react</h1>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode
5. Updated App.js component
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom'

import { App } from './components/App'

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
)
Enter fullscreen mode Exit fullscreen mode
6. Updated root component

Setting up the Supabase Client Library

First, install the Supabase JavaScript client library on your project:

npm install @supabase/supabase-js
Enter fullscreen mode Exit fullscreen mode
7. Install the Supabase client

Now add the code to initialize Supabase on src/supabase.js:

// src/supabase.js

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

const supabase = createClient(
  process.env.REACT_APP_SUPABASE_URL,
  process.env.REACT_APP_SUPABASE_PUBLIC_KEY
)

export { supabase }
Enter fullscreen mode Exit fullscreen mode
8. Supabase client initalization

In your .env.local file, add the URL and Public Anonymous API Key saved from the first step:

# .env.local

REACT_APP_SUPABASE_URL="https://YOUR_SUPABASE_URL.supabase.co"
REACT_APP_SUPABASE_PUBLIC_KEY="eyJKhbGciOisJIUzI1Nd2iIsInR5cCsI6..."
Enter fullscreen mode Exit fullscreen mode
9. Set Supabase configuration variables

Create authentication pages

Let's write the code for the Signup, Login and Dashboard components. These will be the three main pages of the application.

For now, let's just focus on writing a boilerplate for those components, without any authentication logic. Start by writing the Signup component:

// src/components/Signup.js

import { useRef, useState } from 'react'

export function Signup() {
  const emailRef = useRef()
  const passwordRef = useRef()

  async function handleSubmit(e) {
    e.preventDefault()

    // @TODO: add sign up logic
  }

  return (
    <>
      <form onSubmit={handleSubmit}>
        <label htmlFor="input-email">Email</label>
        <input id="input-email" type="email" ref={emailRef} />

        <label htmlFor="input-password">Password</label>
        <input id="input-password" type="password" ref={passwordRef} />

        <br />

        <button type="submit">Sign up</button>
      </form>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode
10. Initial code for the Signup component

The Login component looks very similar to Signup, with a few differences:

// src/components/Login.js

import { useRef, useState } from 'react'

export function Login() {
  const emailRef = useRef()
  const passwordRef = useRef()

  async function handleSubmit(e) {
    e.preventDefault()

    // @TODO: add login logic
  }

  return (
    <>
      <form onSubmit={handleSubmit}>
        <label htmlFor="input-email">Email</label>
        <input id="input-email" type="email" ref={emailRef} />

        <label htmlFor="input-password">Password</label>
        <input id="input-password" type="password" ref={passwordRef} />

        <br />

        <button type="submit">Login</button>
      </form>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode
11. Initial code for the Login component

The Dashboard is a simple component that displays a greeting message and offers the user to sign out:

// src/components/Dashboard.js

export function Dashboard() {
  async function handleSignOut() {
    // @TODO: add sign out logic
  }

  return (
    <div>
      <p>Welcome!</p>
      <button onClick={handleSignOut}>Sign out</button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode
12. Initial code for the Dashboard component

Routing components with React Router

So far the components are isolated. There is no routing between the Signup, Login and Dashboard pages.

Let's work on that by adding React Router to the project:

npm install react-router-dom
Enter fullscreen mode Exit fullscreen mode
13. Install React Router

In src/components/App.js, declare a route for each of the components created before:

// src/components/App.js

import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'

import { Signup } from './Signup'
import { Login } from './Login'
import { Dashboard } from './Dashboard'

export function App() {
  return (
    <div>
      <h1>supabase-auth-react</h1>

      {/* Add routes hereπŸ‘‡ */}
      <Router>
        <Switch>
          <Route exact path="/" component={Dashboard} />
          <Route path="/signup" component={Signup} />
          <Route path="/login" component={Login} />
        </Switch>
      </Router>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode
14. Declare routes in the App component

Let's also add links to navigate between the Signup and Login components:

// src/components/Signup.js

import { Link } from 'react-router-dom'

export function Signup() {
  // ...

  return (
    <>
      <form onSubmit={handleSubmit}>{/* ... */}</form>

      <br />

      {/* Add this πŸ‘‡ */}
      <p>
        Already have an account? <Link to="/login">Log In</Link>
      </p>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode
15. Add link between Signup and Login pages
// src/components/Login.js

import { Link } from 'react-router-dom'

export function Login() {
  // ...

  return (
    <>
      <form onSubmit={handleSubmit}>{/* ... */}</form>

      <br />

      {/* Add this πŸ‘‡ */}
      <p>
        Don't have an account? <Link to="/signup">Sign Up</Link>
      </p>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode
16. Add link between Signup and Login pages

You can test the navigation between components by running the project and clicking on the links or changing the URL in the navigation bar:

17. Demo of the navigation working

Shameless plug: you may notice that the HTML looks a bit different, that's because I am using axist, a tiny drop-in CSS library that I built.

Adding the authentication logic

To set up the authentication logic for the app, we're going to use React's Context API.

The Context API allows sharing data to a tree of components without explicitly passing props through every level of the tree. It's used to share data that is considered "global" (within that component tree).

You can read more about React Context in the official documentation.

In this tutorial, we will use Context to share data associated with the user and the authentication operations. All this information will come from Supabase and will be needed on multiple parts of the app.

Let's start by adding code on src/contexts/Auth.js. First, create a Context object:

// src/contexts/Auth.js

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

const AuthContext = React.createContext()

// ...
Enter fullscreen mode Exit fullscreen mode
18. Create the Context object

Now, in the same file, create a Provider component called AuthProvider:

// src/contexts/Auth.js

// ...

export function AuthProvider({ children }) {
  const [user, setUser] = useState()
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    // Check active sessions and sets the user
    const session = supabase.auth.session()

    setUser(session?.user ?? null)
    setLoading(false)

    // Listen for changes on auth state (logged in, signed out, etc.)
    const { data: listener } = supabase.auth.onAuthStateChange(
      async (event, session) => {
        setUser(session?.user ?? null)
        setLoading(false)
      }
    )

    return () => {
      listener?.unsubscribe()
    }
  }, [])

  // Will be passed down to Signup, Login and Dashboard components
  const value = {
    signUp: (data) => supabase.auth.signUp(data),
    signIn: (data) => supabase.auth.signIn(data),
    signOut: () => supabase.auth.signOut(),
    user,
  }

  return (
    <AuthContext.Provider value={value}>
      {!loading && children}
    </AuthContext.Provider>
  )
}

// ...
Enter fullscreen mode Exit fullscreen mode
19. Create the AuthProvider

The AuthProvider does three things:

  • Calls supabase.auth.session to find out the current state of the user and update the user object.
  • Listens for changes in the authentication state (user signed in, logged out, created a new account, etc.) by subscribing to supabase.auth.onAuthStateChange function.
  • Prepares the object that will be shared by its children components (value prop). In this case, any components down the tree will have access to the signUp, signIn, signOut functions and the user object. They will be used by the Signup, Login and Dashboard components later on.

The loading state property will make sure the child components are not rendered before we know anything about the current authentication state of the user.

Now, create a useAuth function to help with accessing the context inside the children components:

// src/contexts/Auth.js

// ...

export function useAuth() {
  return useContext(AuthContext)
}
Enter fullscreen mode Exit fullscreen mode
20. Create the useAuth function

You can check how the src/contexts/Auth.js looks after all the changes on the GitHub repository.

Lastly, we need to wrap the Signup, Login and Dashboard components with the AuthProvider:

// src/components/App.js

// ...

import { AuthProvider } from '../contexts/Auth'

export function App() {
  return (
    <div>
      <h1>supabase-auth-react</h1>
      <Router>
        {/* Wrap routes in the AuthProvider πŸ‘‡ */}
        <AuthProvider>
          <Switch>
            <PrivateRoute exact path="/" component={Dashboard} />
            <Route path="/signup" component={Signup} />
            <Route path="/login" component={Login} />
          </Switch>
        </AuthProvider>
      </Router>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode
21. Add the AuthProvider to App.js

Adding authentication to the components

Remember the @TODOs you left earlier in the components? Now it's time to, well, do them.

The functions needed by the components - signUp, signIn and signOut - as well as the user object are available through the Context. We can now get those values using the useAuth function.

Let's start by adding the sign up logic to the Signup component:

// src/components/Signup.js

import { useRef, useState } from 'react'
import { useHistory, Link } from 'react-router-dom'

import { useAuth } from '../contexts/Auth'

export function Signup() {
  const emailRef = useRef()
  const passwordRef = useRef()

  // Get signUp function from the auth context
  const { signUp } = useAuth()

  const history = useHistory()

  async function handleSubmit(e) {
    e.preventDefault()

    // Get email and password input values
    const email = emailRef.current.value
    const password = passwordRef.current.value

    // Calls `signUp` function from the context
    const { error } = await signUp({ email, password })

    if (error) {
      alert('error signing in')
    } else {
      // Redirect user to Dashboard
      history.push('/')
    }
  }

  return (
    <>
      <form onSubmit={handleSubmit}>{/* ... */}</form>

      <br />

      <p>
        Already have an account? <Link to="/login">Log In</Link>
      </p>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode
22. Add authentication logic to the Signup component

The Login component will look very similar. The main difference is that you will call signIn instead of signUp:

// src/components/Login.js

import { useRef, useState } from 'react'
import { useHistory, Link } from 'react-router-dom'

import { useAuth } from '../contexts/Auth'

export function Login() {
  const emailRef = useRef()
  const passwordRef = useRef()

  // Get signUp function from the auth context
  const { signIn } = useAuth()

  const history = useHistory()

  async function handleSubmit(e) {
    e.preventDefault()

    // Get email and password input values
    const email = emailRef.current.value
    const password = passwordRef.current.value

    // Calls `signIn` function from the context
    const { error } = await signIn({ email, password })

    if (error) {
      alert('error signing in')
    } else {
      // Redirect user to Dashboard
      history.push('/')
    }
  }

  return (
    <>
      <form onSubmit={handleSubmit}>{/* ... */}</form>

      <br />

      <p>
        Don't have an account? <Link to="/signup">Sign Up</Link>
      </p>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode
23. Add authentication logic to the Login component

Lastly, change the Dashboard so the user can sign out of the application. You can also display some basic information together with the greeting message, such as the user ID:

// src/components/Dashboard.js

import { useHistory } from 'react-router'
import { useAuth } from '../contexts/Auth'

export function Dashboard() {
  // Get current user and signOut function from context
  const { user, signOut } = useAuth()

  const history = useHistory()

  async function handleSignOut() {
    // Ends user session
    await signOut()

    // Redirects the user to Login page
    history.push('/login')
  }

  return (
    <div>
      {/* Change it to display the user ID too πŸ‘‡*/}
      <p>Welcome, {user?.id}!</p>
      <button onClick={handleSignOut}>Sign out</button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode
24. Add authentication logic to the Dashboard component

Protecting routes

Currently, all the authentication logic is in place, but the Dashboard component remains publicly accessible. Anyone who happens to fall on locahost:3000 would see a broken version of the dashboard.

Let's fix that by protecting the route. If a user that is not authenticated tries to access it, they will be redirected to the login page.

Start by creating a PrivateRoute component:

// src/components/PrivateRoute.js

import React from 'react'
import { Route, Redirect } from 'react-router-dom'

import { useAuth } from '../contexts/Auth'

export function PrivateRoute({ component: Component, ...rest }) {
  const { user } = useAuth()

  return (
    <Route
      {...rest}
      render={(props) => {
        // Renders the page only if `user` is present (user is authenticated)
        // Otherwise, redirect to the login page
        return user ? <Component {...props} /> : <Redirect to="/login" />
      }}
    ></Route>
  )
}
Enter fullscreen mode Exit fullscreen mode
25. Create the PrivateRoute component

The PrivateRoute wraps the Route component from React Router and passes down the props to it. It only renders the page if the user object is not null (the user is authenticated).

If the userobject is empty, a redirect to the login page will be made by Redirect component from React Router.

Finally, update the dashboard route in the App component to use a PrivateRoute instead:

// src/components/App.js

- <Route exact path="/" component={Dashboard} />
+ <PrivateRoute exact path="/" component={Dashboard} />
Enter fullscreen mode Exit fullscreen mode
26. Update the Dashboard component to use PrivateRoute

Done! The dashboard is only available for authenticated users.

Final result

This is how the final version of the application should look like:

26. Final demo of the application

You can see the sign up, login, and sign out working. The dashboard page is also protected, attempting to access it by changing the URL redirects the user to the login page. Notice the user ID showing there too.

Going further

There are a few things that we could add for a more complete authentication flow:

Password reset. I intentionally left it out for simplicity, but Supabase supports password reset through their API. It does all the heavy-lifting for you, including sending the email to the user with the reset instructions.

Authentication providers. When logging in, you can also specify different authentication providers like Google, Facebook and GitHub. Check out the docs.

User operations. You can also add metatada to the authenticated user using the update function. With this, you could build for example a basic user profile page.

Thanks for reading this far!

πŸ’– πŸ’ͺ πŸ™… 🚩
ruanmartinelli
Ruan Martinelli

Posted on April 7, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related