Introduction
Serverless databases are all the rage now as they allow you to spin up a fully functional app without building a server or writing a single server code. A Serverless database is a cloud computing system where resources are dynamically distributed and managed.
A few examples of this type of database include Google's Firebase, DynamoDB, Supabase, etc.
In this tutorial, we will be looking at Supabase and integrating it into a react CRUD app.
Prerequisite for this tutorial:
- Github account
- Experience with React.js
- Context API
- Basic understanding of async functions
What Is Supabase?
React js developers are already familiar with what is a CRUD app, but if it the concept of Supabase seems a bit vague, here is an explanation.
Supabase is an open-source serverless database built on PostgreSQL. PostgreSQL is an object-relational database system with over 30 years of active development that has earned it a strong reputation for reliability and robust performance. It is built as an alternative to Google's Firebase and can serve as an excellent Firebase CRUD React platform.
Supabase comes with many out-of-the-box services/functionalities which are there to make life easier for you. These are but are not limited to:
- Authentication: When a user signs up, Supabase creates a new user in the auth.users table. A JWT(JSON Web Token) is returned, containing the user's UID(Unique Identification); subsequent requests to the database also send the JWT. Postgres inspects the JWT to determine which user is making the request.
- Policies: These are PostgreSQL rule engines. They are used to set restrictions and rules on a table or row, allowing you to write SQL rules which fit your needs. These rules can be matched with users' UID to enforce Read/Write access to specific data on the table.
- RLS(Row Level Security): RLS is a system that allows you to restrict access to rows in your tables. Supabase makes it easy for you to turn on PostgreSQL's RLS, literally with the click of a button.
- Realtime Database: Supabase extends Postgres with real-time functionalities, allowing you to listen to changes on the database.
You get all of these features/services out of the box when you create a project on Supabase.
Setting Up A Project
Create an Account with Supabase
The first thing we need to do here is to create an account with Supabase. You will need a Github account to proceed from this point; if you do not have one, you can create an account using the link from the prerequisite section.
Once you log in, you should see a “New project” button; click on this to continue. Then select an organization or create a new organization. Once done with the process, you should see a form with the database name and password.
Fill this and enter a strong password. Click on the “Create new project” button, and wait for the database creation to complete. This process might take a few minutes. Once the operation concluded, click on the table editor from the left panel to create a new table.
Then click on the “Create new table” button, fill in table details, and click Save to create your table. When saving a table is completed, you can preview it from the table editor page.
Now you are ready to add columns to your table. By default, you create a table with an id column.
To create an additional column for your table, click on the “+” button:
On the next step, you will notice a form with the column's name, description, a drop-down with all the variable-type to select from, and default value. If you've filled the form fields, click the "Save" button to proceed.
For the project, we will be creating three additional columns:
- item, with variable type varchar and allow nullable disabled.
- done, with variable type boolean and default value false.
- userId, with the variable type UUID.
Once you create the columns, the next step is to set up our React app with Supabase.
Create the React App
We can use create-react-app to initialize an app called react_supabase. In your terminal, run the following command to create the React app and install the required dependency.
npx create-react-app react_supabase --use-yarn
cd react_supabase
yarn add @supabase/supabase-js bootstrap react-router-dom
Following installing all the dependencies, we will clean up the file structure and create all the required files for the project. The file structure for the React app should now look something similar to the sample below:
📦react_supabase
┣ 📂node_modules
┣ 📂public
┣ 📂src
┃ ┣ 📂components
┃ ┃ ┣ 📜ActiveList.js
┃ ┃ ┣ 📜DoneList.js
┃ ┃ ┣ 📜Navbar.js
┃ ┃ ┣ 📜TodoItem.js
┃ ┃ ┗ 📜UpdateItem.js
┃ ┣ 📂pages
┃ ┃ ┣ 📜Home.js
┃ ┃ ┗ 📜Login.js
┃ ┣ 📜App.js
┃ ┣ 📜App.test.js
┃ ┣ 📜ItemsContext.js
┃ ┣ 📜index.css
┃ ┣ 📜index.js
┃ ┣ 📜reportWebVitals.js
┃ ┣ 📜setupTests.js
┃ ┗ 📜supabaseClient.js
┣ 📜.env.local
┣ 📜.gitignore
┣ 📜README.md
┣ 📜package.json
┗ 📜yarn.lock
Now we are ready to start writing some code.
In the App.js file, we will instantiate the routing for the login page and home page like this:
import "bootstrap/dist/css/bootstrap.css";
import { useEffect } from "react";
import { Route, Switch, useHistory, withRouter } from "react-router-dom";
import Navbar from "./components/Navbar";
import Home from "./pages/Home";
import Login from "./pages/Login";
import { supabase } from "./supabaseClient";
function App() {
const history = useHistory();
useEffect(() => {
supabase.auth.onAuthStateChange((_event, session) => {
if (session === null) {
history.replace("/login");
} else {
history.replace("/");
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const NavbarWithRouter = withRouter((props) => <Navbar {...props} />);
return (
<>
<NavbarWithRouter exact />
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/login" component={Login} />
</Switch>
</>
);
}
export default App;
If you notice the useEffect hook, we have a supabase.auth.onAuthStateChange; this listens to changes in the auth/session state. The code block in the useEffect checks if there is no active session; if this is true, the app will redirect you to the login route; otherwise, it turns to the home route. The code block ensures only authenticated users can access the home route.
Another thing worth mentioning is the withRouter, a higher-order function that allows you to pass router props to a standalone component, the Navbar component.
After this, we need to wrap the App component imported in the index.js file with BrowserRouter.
... <React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>...
Setup Supabase
To set up Supabase, we need our API URL and the Supabase anon key; these are available on your Supabase dashboard. From the left pane on your dashboard, click on API; from here, navigate to the Authentication page. You will find your database URL and anon key.
Copy the database URL and your anon key into the .env.local file.
REACT_APP_SUPABASE_URL='your supabase url'
REACT_APP_SUPABASE_ANON_KEY='your anon key'
Inside the supabaseClient file import, createClient from @supabase/supabase-js, initialize a variable supabase and call the createClient function with the supabase URL and anon key as parameters.
import { createClient } from "@supabase/supabase-js";
export const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY)
Setup Context State
Now that we have the Supabase client setup, the subsequent procedure is to set up our state management inside the ItemsContext file.
import React, { createContext, useState } from "react";
import { supabase } from "./supabaseClient";
// Initializing context
export const ItemsContext = createContext();
export function ItemsContextProvider({ children }) {
const [activeItems, setActiveItems] = useState([]);
const [inactiveItems, setInactiveItems] = useState([]);
const [loading, setLoading] = useState(false);
const [adding, setAdding] = useState(false);
...
return (
<ItemsContext.Provider
value={{
activeItems,
inactiveItems,
loading,
adding,
...
}}
>
{children}
</ItemsContext.Provider>
);
}
Now wrap your entire app with the ItemsContextProvider to complete the set up.
...
<React.StrictMode>
<ItemsContextProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</ItemsContextProvider>
</React.StrictMode>
...
Handling Authentication
It is time to start working on the core functionalities for our app.
We will be starting with authentication and the login page. For authentication, we will be using the magic link auth method from Supabase.
Supabase also supports a range of other external authentication methods, including:
- Apple
- Azure
- Bitbucket
- Discord
- GitHub
- GitLab
- Twitch
To get started with authentication, copy and paste the code block below inside your context component:
// Authentication function for logging in new/old user with supabase magic link
const logInAccount = async (email) => {
setLoading(true);
try {
// supabase method to send the magic link to the email provided
const { error } = await supabase.auth.signIn({ email });
if (error) throw error; //check if there was an error fetching the data and move the execution to the catch block
alert("Check your email for your magic login link!");
} catch (error) {
alert(error.error_description || error.message);
} finally {
setLoading(false);
}
};
If you review the code block above, the function accepts one parameter: the user's email address. Supabase signIn receives this email address as a parameter. Once the request is successful, a mail is sent to the email address with an authentication link, and like "magic," you're logged in when you click on the link.
It might be worth noting that you can change the authentication redirect URL on your Supabase dashboard; the default URL is http://localhost:3000.
Let's create the login page and consume the authentication function. Copy and paste the code block below in your Login.js file.
import React, { useContext, useEffect, useState } from "react";
import { useHistory } from "react-router-dom";
import { ItemsContext } from "../ItemsContext";
import { supabase } from "../supabaseClient";
export default function Login() {
const history = useHistory();
const [email, setEmail] = useState("");
const { loading, logInAccount } = useContext(ItemsContext);
const handleSubmit = (e) => {
e.preventDefault();
logInAccount(email);
};
useEffect(() => {
if (supabase.auth.user() !== null) {
history.replace("/");
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className="container">
<div className="row justify-content-center mt-5">
<div className=" col-12 col-lg-6">
<div className="card">
<div className="card-header">
<h5 className="text-center text-uppercase">Log In</h5>
</div>
<div className="card-body">
<form onSubmit={handleSubmit}>
<div className="mb-4">
<label htmlFor="exampleInputEmail1" className="form-label">
Email address
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
name="email"
required
className="form-control form-control-lg w-100 mt-1"
/>
<div className="form-text">
Enter your email to get your magic link
</div>
</div>
<button disabled={loading} type="submit" className="btn btn-primary btn-lg w-100 ">
{loading ? "Loading..." : "Submit"}
</button>
</form>
</div>
</div>
</div>
</div>
</div>
);
}
Adding an Item
To add an item, we will be using another function with the Supabase insert method. You will pass in an object with keys matching the name of columns created on your table.
...
const user = supabase.auth.user();
await supabase.from("todo").insert({ item, userId: user?.id }); //insert an object with the key value pair, the key being the column on the table
...
First, you call the from method and pass in the table's name to insert the data. The supabase.auth.user() allows you to get the authenticated user's details. The code above will insert a new row to the to-do table with the item's value and userId with the authenticated UUID.
Reading To-Do Items From the Database
Once we have created a row on the table, we can now read data from the table. To do this, we will be using another Supabase method, select with additional query params to filter through data.
...
// get the user currently logged in
const user = supabase.auth.user();
const { error, data } = await supabase
.from("todo") //the table you want to work with
.select("item, done, id") //columns to select from the database
.eq("userId", user?.id) //comparison function to return only data with the user id matching the current logged in user
.eq("done", false) //check if the done column is equal to false
.order("id", { ascending: false }); // sort the data so the last item comes on top;
if (error) throw error; //check if there was an error fetching the data and move the execution to the catch block
if (data) setActiveItems(data);
...
The select() method accepts a string as params with the columns to return. After that, we have the eq() method, which takes two parameters; the First parameter is the column's name to compare, and the second parameter is the value to match. The code block above will return the item, done, and id column from rows where the userId column matches the authenticated UUID.
Updating a To-Do Item
We will use the function below to update a row; We create an async function that receives two parameters; the new value to update and the row's id to update. We will use the Supabase update method, which accepts an object with the column name as a key and the new value.
// update column(s) on the database
const updateItem = async ({ item, id }) => {
setLoading(true);
try {
const user = supabase.auth.user();
const { error } = await supabase
.from("todo")
.update({ item })
.eq("userId", user?.id)
.eq("id", id); //matching id of row to update
if (error) throw error;
await getActiveItems();
} catch (error) {
alert(error.error_description || error.message);
} finally {
setLoading(false);
}
};
Toggling an Item as Done
Toggling the item as done is very similar to the update function, except we already know the new value and the field to update. All we need to pass in is the row's id. We will use a function similar to the update button to toggle the done column on the table as either true or false.
// change value of done to true
....
const { error } = await supabase.from("todo")
.update({ done: true })
.eq("userId", user?.id)
.eq("id", id); //match id to toggle
...
Deleting an Item
To delete an item from the database, copy and paste the code block below:
// delete row from the database
const deleteItem = async (id) => {
try {
const user = supabase.auth.user();
const { error } = await supabase
.from("todo")
.delete() //delete the row
.eq("id", id) //the id of row to delete
.eq("userId", user?.id) //check if the item being deleted belongs to the user
if (error) throw error;
await getInactiveItems(); //get the new completed items list
await getActiveItems(); //get the new active items list
} catch (error) {
alert(error.error_description || error.message);
}
};
The deleteItem function receives one parameter: the row id to delete from; the eq query compares the id to find the matching row. Once the request completes with a success status, Supabase will delete the matching row from our table.
Conclusion
Congratulations on making it this far! At this moment, you have all the functions to complete a React CRUD app operation on your React app. You can find the complete project code in this GitHub repository or test the live project here. For additional information on Supabase, you can check out their official documentation here.