Welcome! You’re about to build your first full-stack React application with a real database. By the end of this project, you’ll have created a potluck meal management app that can store, display, and manage data - just like the apps you use every day!
What you’ll learn:
How this works:
Each level includes:
Goal: Plan your potluck app using the activity guide.
User Story: As a developer, I want to plan my database structure and components so that I can build my potluck management app efficiently.
Complete the Activity Guide first! Work through Steps 1-4 of the Activity Guide to:
This planning will help you build your app more efficiently!
Take time to plan your potluck app using the structured activity guide.
Key Planning Questions:
Time Investment: Spend 15-20 minutes on planning - it will save you hours of debugging later!
Attribution: This project guide was created with assistance from Claude AI (Anthropic).
Goal: Set up your development environment with Vite and React.
User Story: As a developer, I want to create a new React project and install dependencies so that I can start building my potluck management app.
Create a new React project using Vite and install the Supabase client library.
npm create vite@latest practice-with-db -- --template reactcd practice-with-dbnpm installnpm install @supabase/supabase-jsnpm install tslibnpm run devSee here for installation details: Install Supabase Client
Need help with project setup? Check out these snippets:
npm create vite@latest practice-with-db -- --template react
cd practice-with-db
npm install
npm install @supabase/supabase-js
practice-with-db/
├── src/
│ ├── components/
│ │ └── PotluckMeals.jsx
│ ├── utils/
│ │ └── supabase.js
│ ├── App.jsx
│ └── main.jsx
├── package.json
└── README.md
Why do we need these libraries?
@supabase/supabase-js: This is the official JavaScript client library for Supabase. It provides all the functions we need to connect to our database, perform queries, and handle authentication. Without it, we’d have to write complex HTTP requests manually.
tslib: This is a TypeScript runtime library that Supabase depends on. Even though we’re using JavaScript, Supabase’s internal code uses TypeScript features, so we need this library to support those features.
Where do these libraries go?
When you run npm install, npm:
node_modules/ folder in your projectpackage.json file to record the dependenciesHow to verify installation:
package.json: Open your package.json file and look for the dependencies section:
{
"dependencies": {
"@supabase/supabase-js": "^2.x.x",
"tslib": "^2.x.x"
}
}
Check node_modules/: Look in your project folder for a node_modules/ directory. Inside, you should see folders named @supabase and tslib.
npm list to see all installed packages and their versions.npm run dev to start your development serverhttp://localhost:5173@supabase/supabase-js is listed in your package.json dependenciesGoal: Configure your Supabase database with tables and security policies.
User Story: As a developer, I want to set up my database tables and security policies so that I can store and retrieve potluck data securely.
Follow the Supabase setup guides to create your database tables and configure access policies.
potluck_meals table with these columns:
meal_name (text)guest_name (text)serves (integer)kind_of_dish (text).env.local file in your project’s root directory instructionssrc/util Note: do not copy the code for App.jsx for this project. We will put the code into a component in later steps instructions⚠️ Important Note: Do NOT use the starter code for App.jsx from the Supabase setup guide. That code puts everything in one file, but we’ll be organizing our app into separate components. Follow the component structure outlined in Levels 4-6 below instead.
What are Row Level Security (RLS) policies?
Understanding our policies:
-- Read policy: Allow everyone to read meals
CREATE POLICY "Enable read access for all users" ON potluck_meals
FOR SELECT USING (true);
-- Insert policy: Allow everyone to add meals
CREATE POLICY "Enable insert for all users" ON potluck_meals
FOR INSERT WITH CHECK (true);
USING (true): For SELECT policies, means “allow if condition is true” (always true = everyone can read)WITH CHECK (true): For INSERT policies, means “allow if condition is true” (always true = everyone can insert)true: Always evaluates to true, so these policies allow public accessEnvironment variables explained:
VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-key-here
VITE_ prefix: Required for Vite to include these variables in your build.env.local: More secure than .env (automatically ignored by git)📺 Learn More:
potluck_meals table exists with the correct columnsGoal: Create the main PotluckMeals component and import it into your App component.
User Story: As a developer, I want to create a component that can fetch and display data from my database so that I can see my potluck meals.
Create the PotluckMeals component with basic structure and import it into your App component.
src/components/PotluckMeals.jsx with basic structureuseState from React../utils/supabasemeals with initial value of empty arraysrc/App.jsxNeed help with component structure? Check out these snippets:
import { useState } from "react"
import supabase from "../utils/supabase"
export default function PotluckMeals() {
const [meals, setMeals] = useState([])
return <>
<h1>Potluck meals</h1>
<button>Fetch Meals</button>
<ul>
{/* Meals will be displayed here */}
</ul>
</>
}
import PotluckMeals from './components/PotluckMeals'
function App() {
return (<>
</>)
}
export default App
Goal: Add a fetch meals function with console logging.
User Story: As a developer, I want to create a button handler so that I can test the connection to my database.
Add the fetch meals function with console logging and connect it to your button.
async function handleFetchMeals() that logs “Fetching meals…” to consoleonClick={handleFetchMeals}Need help with the button handler? Check out these snippets:
async function handleFetchMeals() {
console.log("Fetching meals...")
// We'll add the actual fetch logic in the next step
}
<button onClick={handleFetchMeals}>Fetch Meals</button>
Goal: Update the fetch function to actually retrieve data from Supabase.
User Story: As a developer, I want to fetch data from my database so that I can see what meals are stored.
Update the handleFetchMeals function to fetch data from Supabase and log it to console.
handleFetchMeals function to use await supabase.from("potluck_meals").select()setMeals(data)Need help with data fetching? Check out these snippets:
async function handleFetchMeals() {
console.log("Fetching meals...")
const result = await supabase.from("potluck_meals").select()
const data = result.data
console.log("Fetched data:", data);
setMeals(data);
}
What is async/await?
async: Marks a function as asynchronous, meaning it can use await and will return a Promiseawait: Pauses execution until a Promise resolves, then returns the resolved valueUnderstanding Supabase queries:
const result = await supabase.from("potluck_meals").select()
supabase.from("table_name"): Specifies which table to query.select(): Retrieves all columns from the table (like SELECT * in SQL)result: Contains both data and error propertiesresult.data: The actual data returned from the databaseresult.error: Any error that occurred during the queryError handling pattern:
Always check for errors when working with databases:
if (result.error) {
console.error('Database error:', result.error);
return; // Stop execution if there's an error
}
📺 Learn More:
Goal: Display the fetched meals using a for loop.
User Story: As a user, I want to see the meals displayed on the page so that I can view what’s planned for the potluck.
Add display logic using a for loop to render each meal in the list.
mealsDisplay array<li> element with the meal information{mealsDisplay} in the JSXNeed help with the display loop? Check out these snippets:
const mealsDisplay = []
for (let i = 0; i < meals.length; i++) {
mealsDisplay.push(
<li key={meals[i].id}>
{meals[i].meal_name} by {meals[i].guest_name} serves {meals[i].serves} ( {meals[i].kind_of_dish} )
</li>
)
}
<ul>
{mealsDisplay}
</ul>
Why do we need the key prop?
id) rather than array indexUnderstanding JSX in loops:
mealsDisplay.push(
<li key={meals[i].id}>
{meals[i].meal_name} by {meals[i].guest_name} serves {meals[i].serves} ( {meals[i].kind_of_dish} )
</li>
)
{variable} to insert JavaScript values into JSXAlternative approaches:
You could also use .map() instead of a for loop:
const mealsDisplay = meals.map(meal => (
<li key={meal.id}>
{meal.meal_name} by {meal.guest_name} serves {meal.serves} ({meal.kind_of_dish})
</li>
));
📺 Learn More:
Goal: Verify that your data fetching and display is working correctly.
User Story: As a developer, I want to test my data fetching functionality so that I can ensure everything is working properly.
Test your data fetching functionality and verify the results.
npm run devGoal: Add a form to your PotluckMeals component for adding new meals.
User Story: As a user, I want to fill out a form to add my dish to the potluck so that others can see what I’m bringing.
Add a form with input fields for meal details to your component.
onSubmit={handleAddMeal} inside your return statement, after the ul elementNeed help with form structure? Check out these snippets:
// Add this inside your return statement, after the ul element
<div>
<form onSubmit={handleAddMeal}>
<label>
Meal: <input type="text" name="mealName" />
</label>
<br/>
<label>
Guest: <input type="text" name="guestName" />
</label>
<br/>
<label>
Serves: <input type="number" name="serves" />
</label>
<br/>
<label>
Kind of Dish: <input type="text" name="kindOfDish" />
</label>
<br/>
<button type="submit">Add Meal</button>
</form>
</div>
Goal: Create the handleAddMeal function to process form submissions.
User Story: As a developer, I want to create a form handler so that I can process the form data when users submit it.
Add the handleAddMeal function to your component to handle form submissions.
async function handleAddMeal(event) that:
event.preventDefault() to prevent page refreshevent.target.elementsnewMeal object with the form dataparseInt(serves)Need help with the form handler? Check out these snippets:
async function handleAddMeal(event){
event.preventDefault()
console.log("handle add meal submitted")
const mealName = event.target.elements.mealName.value
const guestName = event.target.elements.guestName.value
const serves = event.target.elements.serves.value
const kindOfDish = event.target.elements.kindOfDish.value
const newMeal = {
meal_name: mealName,
guest_name: guestName,
serves: parseInt(serves),
kind_of_dish: kindOfDish
}
console.log(newMeal)
// We'll add the insert logic in the next step
}
Understanding form events:
event.preventDefault(): Stops the browser’s default form submission behavior (page refresh)event.target: References the form element that triggered the eventevent.target.elements: Collection of all form input elements with name attributesExploring the event object:
To better understand how events work, try adding this to your form handler:
function handleAddMeal(event) {
console.log(event); // Examine the entire event object
console.log(event.target); // Look at the form element
console.log(event.target.elements); // See all form inputs
event.preventDefault();
// ... rest of your code
}
What you’ll see in the console:
event: A large object with many properties (type, target, preventDefault, etc.)event.target: The <form> element that was submittedevent.target.elements: A collection of all inputs with name attributesForm data extraction pattern:
const mealName = event.target.elements.mealName.value
const guestName = event.target.elements.guestName.value
elements.mealName: Gets the input with name="mealName".value: Gets the current value from that inputuseState for each field (controlled inputs), but this is simplerData type conversion:
serves: parseInt(serves)
parseInt(): Converts string to integerparseInt("abc") returns NaN - in production, you’d validate thisObject creation pattern:
const newMeal = {
meal_name: mealName,
guest_name: guestName,
serves: parseInt(serves),
kind_of_dish: kindOfDish
}
meal_name) vs. form field names (mealName)📺 Learn More:
Goal: Set up a write policy for your potluck_meals table.
User Story: As a developer, I want to configure database security policies so that users can insert new meals into the database.
Follow the Supabase Setup Guide to set up a write policy for your potluck_meals table.
true to the “with check” statement in the policyThis allows anyone to create new rows in your table.
🔒 Cybersecurity Reflection: Think about this statement from a security perspective. What are the potential risks of allowing “anyone” to create new rows in your database? Consider:
Note: This policy is intentionally permissive for learning purposes. In production applications, you would implement proper authentication and authorization controls.
true in the “with check” statementGoal: Update your form handler to insert new meals into the database.
User Story: As a user, I want my form submission to save the meal to the database so that it persists and can be retrieved later.
Update your handleAddMeal function to include the database insertion logic.
handleAddMeal function to include the insert logicawait supabase.from("potluck_meals").insert(newMeal) to insert the new mealNeed help with database insertion? Check out these snippets:
async function handleAddMeal(event){
event.preventDefault()
console.log("handle add meal submitted")
const mealName = event.target.elements.mealName.value
const guestName = event.target.elements.guestName.value
const serves = event.target.elements.serves.value
const kindOfDish = event.target.elements.kindOfDish.value
const newMeal = {
meal_name: mealName,
guest_name: guestName,
serves: parseInt(serves),
kind_of_dish: kindOfDish
}
console.log(newMeal)
// Insert the new meal
await supabase.from("potluck_meals").insert(newMeal)
}
Understanding CRUD operations:
INSERT - Adding new records to the databaseSELECT - Retrieving data from the databaseUPDATE - Modifying existing recordsDELETE - Removing records from the databaseDatabase insertion explained:
await supabase.from("potluck_meals").insert(newMeal)
supabase.from("table_name"): Specifies which table to insert into.insert(data): Inserts the provided data as a new rowawait: Waits for the insertion to complete before continuingExploring network requests:
To see what’s actually being sent to Supabase, open your browser’s Developer Tools:
F12 or right-click → “Inspect”newMeal object)What you’ll see:
https://your-project.supabase.co/rest/v1/potluck_mealsPOST (for inserting data)Data persistence:
Error handling considerations:
In production apps, you’d want to handle potential errors:
const { data, error } = await supabase.from("potluck_meals").insert(newMeal)
if (error) {
console.error('Insert failed:', error);
// Show user-friendly error message
} else {
console.log('Meal added successfully:', data);
// Update UI to show success
}
Database constraints:
📺 Learn More:
Goal: Verify that your insert functionality is working correctly.
User Story: As a developer, I want to verify that my data insertion is working so that I can ensure the database is being updated properly.
Test your insert functionality and verify the results in the Supabase dashboard.
Goal: Refresh the meals list after inserting a new meal.
User Story: As a user, I want to see my new meal appear in the list immediately after submitting the form so that I can confirm it was added.
Update your handleAddMeal function to refresh the meals list after inserting.
handleAddMeal function to refresh the meals list after insertingsupabase.from("potluck_meals").select()setMeals(data)Need help with refreshing the list? Check out these snippets:
async function handleAddMeal(event){
event.preventDefault()
console.log("handle add meal submitted")
const mealName = event.target.elements.mealName.value
const guestName = event.target.elements.guestName.value
const serves = event.target.elements.serves.value
const kindOfDish = event.target.elements.kindOfDish.value
const newMeal = {
meal_name: mealName,
guest_name: guestName,
serves: parseInt(serves),
kind_of_dish: kindOfDish
}
console.log(newMeal)
// Insert the new meal
await supabase.from("potluck_meals").insert(newMeal)
// Refresh the meals list
const response = await supabase.from("potluck_meals").select()
const data = response.data
setMeals(data)
}
Goal: Clear the form inputs after successful submission.
User Story: As a user, I want the form to clear after I submit it so that I can easily add another meal.
Update your handleAddMeal function to clear the form inputs after submission.
handleAddMeal function to clear the form inputs after successful submissionevent.target.elements to access each input fieldNeed help with clearing inputs? Check out these snippets:
async function handleAddMeal(event){
event.preventDefault()
console.log("handle add meal submitted")
const mealName = event.target.elements.mealName.value
const guestName = event.target.elements.guestName.value
const serves = event.target.elements.serves.value
const kindOfDish = event.target.elements.kindOfDish.value
const newMeal = {
meal_name: mealName,
guest_name: guestName,
serves: parseInt(serves),
kind_of_dish: kindOfDish
}
console.log(newMeal)
// Insert the new meal
await supabase.from("potluck_meals").insert(newMeal)
// Refresh the meals list
const response = await supabase.from("potluck_meals").select()
const data = response.data
setMeals(data)
// Clear the form inputs
event.target.elements.mealName.value = ""
event.target.elements.guestName.value = ""
event.target.elements.serves.value = ""
event.target.elements.kindOfDish.value = ""
}
Goal: Replace the text input for dish type with a dropdown for better data consistency.
User Story: As a user, I want to select from predefined dish types so that the data is consistent and easier to filter.
Replace the “Kind of Dish” text input with a select dropdown containing predefined options.
defaultValue="" on the select elementNeed help with select dropdown? Check out these snippets:
<label>
Kind of Dish:
<select name="kindOfDish" defaultValue="">
<option value="" disabled>Select a kind</option>
<option value="entree">Entree</option>
<option value="side">Side</option>
<option value="snack">Snack</option>
<option value="dessert">Dessert</option>
<option value="drink">Drink</option>
</select>
</label>
Goal: Create another table and component for Beverages following the same pattern as meals.
User Story: As a developer, I want to create additional tables and components so that I can manage different types of potluck items.
Create a beverages table and component following the same pattern as the meals component.
beverages table with appropriate columns (e.g., beverage_name, guest_name, quantity, type_of_drink)Beverages.jsx component following the same pattern as PotluckMeals.jsxGoal: Create a table and component for Utensils following the same pattern.
User Story: As a developer, I want to create additional tables and components so that I can manage different types of potluck items.
Create a utensils table and component for managing potluck utensils.
item_name, guest_name, quantity, item_type)Utensils.jsx component following the same patternCHALLENGE LEVEL
User Story: As a developer, I want to implement advanced features so that I can create a more impressive and functional app.
Choose from these advanced challenges to extend your potluck app.
Complete at least 2 of the following challenges:
Remember that Level 16 (select dropdown) is also a challenge that you can implement!
Create another table that you think would improve your app (e.g., dietary restrictions, allergies, etc.). Decide on the columns yourself.
Style your app using CSS or Bootstrap to make it more visually appealing.
Add conditional styling based on the type of dish or other data.
Use elements other than <li> tags to display your items (cards, tables, etc.).
Break your app into smaller, reusable components with props.
Have an option to upload a file. Consider using Cloudinary and an “upload url”. This will take some research.
Congratulations! You’ve successfully built a complete React + Supabase potluck management app!
You’ve built a fully functional app that demonstrates:
Through this project, you’ve gained hands-on experience with:
After completing this project, consider exploring:
Consider:
You’ve completed a significant milestone in your React and database journey. Your potluck app demonstrates your ability to build full-stack web applications with modern tools.
Keep building, keep learning, and keep growing as a developer!
Project Status: Complete! ✨
Environment Variables Not Loading: Make sure your .env.local file is in the project root and variables start with VITE_
RLS Policy Errors: Check SQL syntax and ensure policies are properly quoted
CORS Issues: Verify your Supabase URL and keys are correct
Form Not Submitting: Ensure you have event.preventDefault() in your handler
console.log() statements to debug data flowAttribution: This project guide was created with assistance from Claude AI (Anthropic).