🗄️ 第十二章:Supabase 動態資料庫串接與渲染

在上一章,我們用極致的視覺特效打造了超美的 Bento Grid。 但目前卡片上的字是「寫死 (Hardcoded)」在 HTML 裡的。如果老闆說要新增一個營地,你總不能每次都去改程式碼然後重新部署吧?

這就是為什麼我們需要 資料庫 (Database)。 在這堂課中,我們將使用現代最強的 Backend-as-a-Service:Supabase。 我們不教你怎麼寫 SQL 語法,而是教你:「如何向 AI 點餐,讓它幫你把資料從 Supabase 撈出來,完美無縫地塞進你的 React 絕美卡片中!」


📡 實戰 1:建立 Supabase 客戶端 (Client) 連線

在跟資料庫要資料之前,我們必須先拿到一把「鑰匙」,告訴 Supabase 我們是誰。

💡 Vibe Prompt 實戰 1:初始化 Supabase 連線

[!IMPORTANT] 請複製以下 Prompt 傳送給 AI:

我正在使用 React 與 Supabase 開發露營網站。 請幫我寫一個 supabaseClient.js 檔案,用來初始化 Supabase 連線。 我的 URL 和 KEY 會存在環境變數中 (.env 檔案裡的 VITE_SUPABASE_URL 和 VITE_SUPABASE_ANON_KEY)。 請給我完整的程式碼,並在註解中說明為什麼要把金鑰放在環境變數中,而不是直接寫死在程式碼裡。

🤖 AI 生成的真實程式碼:

// supabaseClient.js
import { createClient } from '@supabase/supabase-js';

// 從環境變數中讀取 Supabase 的專案 URL 和公用 API 金鑰
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;

// 建立並匯出 Supabase 客戶端實體
export const supabase = createClient(supabaseUrl, supabaseAnonKey);

/*
【AI 資安提醒】:
為什麼不能把金鑰寫死在這裡?(例如直接寫 "https://xxx.supabase.co")
因為如果你把程式碼推上 GitHub (特別是公開的 Repository),
全世界的駭客都能看到你的資料庫金鑰,進而刪除或竄改你的所有資料!
把它放在 .env 檔案中,並在 .gitignore 裡排除 .env,是保護資安的第一道防線!
*/

🎣 實戰 2:從資料庫撈取資料 (Fetch Data)

我們在 Supabase 裡面建了一個名為 campgrounds 的資料表 (Table)。 裡面有 name (名稱)、location (地點)、price (價格)。 現在我們要把它撈出來!

💡 Vibe Prompt 實戰 2:撰寫非同步抓取函數

[!IMPORTANT] 請複製以下 Prompt 傳送給 AI:

請使用我剛剛建立的 supabaseClient。 幫我寫一個 async 函數 getCampgrounds(),用來從 Supabase 的 "campgrounds" 資料表中撈取所有資料。 如果抓取成功,請回傳資料的陣列 (Array)。 如果發生錯誤,請在 console 印出錯誤訊息,並回傳一個空陣列 [],確保畫面不會崩潰。 請加上詳細的中文註解。

🤖 AI 的高階防禦性程式碼:

import { supabase } from './supabaseClient';

export const getCampgrounds = async () => {
  try {
    // 使用 supabase.from('資料表名稱').select('*') 來抓取所有欄位
    // 這是一個需要花時間的動作,所以前面必須加上 await!
    const { data, error } = await supabase
      .from('campgrounds')
      .select('*');

    // Supabase 的 API 設計很特別,即便出錯也不會直接當機,而是會把錯誤訊息裝在 error 變數裡
    if (error) {
      // 如果 error 裡面有東西,代表出錯了!我們主動把錯誤丟出去給 catch 處理
      throw error;
    }

    // 如果一切順利,回傳拿到的資料陣列
    return data;
    
  } catch (error) {
    // 安全氣囊啟動:攔截錯誤,印出警告,並回傳空陣列
    console.error('抓取營地資料失敗 ❌:', error.message);
    return []; 
  }
};

⏳ 實戰 3:前端狀態管理 (State) 與載入中 (Loading)

這是一道網頁工程師的經典面試題:「網路連線是需要時間的,在這 1~2 秒的空檔,使用者的畫面會發生什麼事?」 如果你沒處理好,使用者會看到一片空白,以為當機了就關掉網頁。 我們必須使用 React 的 useStateuseEffect 來管理「載入中」的狀態!

💡 Vibe Prompt 實戰 3:完整的 React 資料流

[!IMPORTANT] 請複製以下 Prompt 傳送給 AI:

我要在 React 元件 <CampList> 中使用剛剛寫好的 getCampgrounds 函數。 請幫我設定三個狀態 (useState): 1. camps (存放營地資料的陣列,預設為空) 2. isLoading (是否正在載入,預設為 true) 3. errorMsg (錯誤訊息,預設為空字串) 請使用 useEffect 在元件第一次載入時去抓資料。 抓完資料後,把 isLoading 設為 false。 畫面渲染邏輯:如果正在載入,顯示 "資料拼命載入中...";如果有錯誤,顯示錯誤訊息;如果都沒問題,先用 JSON.stringify 把資料印出來看看。

🤖 AI 的完美狀態管理機:

import React, { useState, useEffect } from 'react';
import { getCampgrounds } from './api';

export default function CampList() {
  // 宣告狀態三兄弟
  const [camps, setCamps] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [errorMsg, setErrorMsg] = useState("");

  // useEffect 的空陣列 [] 代表「這個元件剛出現在畫面上時,只執行一次」
  useEffect(() => {
    const fetchData = async () => {
      // 1. 開始抓資料,將狀態設為載入中
      setIsLoading(true);
      
      // 2. 去呼叫我們寫好的 Supabase API
      const data = await getCampgrounds();
      
      // 3. 判斷有沒有拿到資料
      if (data && data.length > 0) {
        setCamps(data); // 把資料存進狀態裡
      } else {
        setErrorMsg("目前沒有營地資料,或是連線發生問題。");
      }
      
      // 4. 不管成功或失敗,最後都要把載入中狀態解除
      setIsLoading(false);
    };

    fetchData();
  }, []);

  // 畫面渲染邏輯 (Conditional Rendering)
  if (isLoading) {
    return <div className="p-10 text-center text-xl text-blue-400 animate-pulse">⏳ 資料拼命載入中...</div>;
  }

  if (errorMsg) {
    return <div className="p-10 text-center text-red-500">❌ {errorMsg}</div>;
  }

  // 資料抓取成功,暫時把原始資料印出來檢查
  return (
    <div className="p-10">
      <h2 className="text-2xl font-bold mb-4">資料庫原始數據</h2>
      <pre className="bg-zinc-900 p-4 rounded text-green-400 overflow-auto">
        {JSON.stringify(camps, null, 2)}
      </pre>
    </div>
  );
}

🎨 實戰 4:把原始資料變成上一章的絕美卡片

現在你的畫面上有一堆綠色的 JSON 原始碼。 我們要結合上一章學到的 Array.map(),把每一筆資料,都變成一張絕美的毛玻璃光暈卡片 (GlowCard)。

💡 Vibe Prompt 實戰 4:陣列 Map 與元件渲染

[!IMPORTANT] 請複製以下 Prompt 傳送給 AI:

現在 camps 陣列裡面有許多物件,例如 { id: 1, name: "星空營地", price: 1500, location: "苗栗" }。 請幫我修改 <CampList> 的最後一段 return。 1. 使用一個 <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> 來包裝。 2. 使用 camps.map() 迴圈。 3. 把每一筆資料傳入上一章寫好的 <GlowCard> 裡面。 4. 在卡片內顯示出營地名稱 (h3)、地點 (p) 和價格 (p,標示為 NT$ xxx / 晚)。 記得加上 React 列表需要的 key 屬性。

🤖 AI 的畫龍點睛:

// 將原本的 <pre> JSON 輸出刪除,換成這個:
return (
  <div className="max-w-6xl mx-auto p-10">
    <h2 className="text-3xl font-bold text-white mb-8">🏕️ 探索熱門營地</h2>
    
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
      {/* 使用 map 迴圈,把每一筆 camp 資料變成一張實體卡片 */}
      {camps.map((camp) => (
        // key 是必需的,讓 React 知道每一張卡片的唯一身份
        <GlowCard key={camp.id}>
          <h3 className="text-2xl font-bold text-white mb-2">{camp.name}</h3>
          <p className="text-zinc-400 mb-4 flex items-center gap-2">
            📍 {camp.location}
          </p>
          <div className="mt-auto pt-4 border-t border-white/10">
            <span className="text-emerald-400 font-bold text-xl">NT$ {camp.price}</span>
            <span className="text-zinc-500 text-sm ml-1">/ 晚</span>
          </div>
        </GlowCard>
      ))}
    </div>
  </div>
);

🔍 深度解析: 看到了嗎?這就是從後端到前端的全端開發!

  1. 你去 Supabase 建立了一個 Table。
  2. 你用 supabaseClient 把資料抓下來。
  3. 你存進 useState 狀態裡,並處理了 Loading。
  4. 你用 .map() 把資料變成了帶有光暈特效的 HTML 結構。

當你完成這四步,你的網站就真正具備了「商業價值」。因為從此以後,老闆要新增商品,他只要去後台填表單,你的前台網站就會自動長出一張超美的卡片,完全不需要修改任何程式碼!


🚫 終極避坑指南:無限迴圈又來了? (useEffect 的坑)

在 Vibe Coding 中,最常導致瀏覽器直接當機的,就是 useEffect 的依賴陣列 (Dependency Array) 設定錯誤。

💡 Vibe Prompt 實戰 5:解決 useEffect 瘋狂觸發的問題

[!IMPORTANT] 請複製以下 Prompt 傳送給 AI:

我的 React 程式寫了 useEffect 去抓 Supabase 資料,然後存入 setCamps。 但我發現瀏覽器的 Network 標籤頁裡面,每秒鐘瘋狂發出幾百個 API 請求!Supabase 直接警告我超過免費額度! 這是我的程式碼: useEffect(() => { fetchData(); }); 請問問題出在哪裡?我該怎麼修復?

🤖 AI 的緊急煞車救援:

/*
❌ 致命錯誤分析:
你的 useEffect 最後面漏寫了「依賴陣列 (Dependency Array)」!
如果你不寫最後面的 `[]`,useEffect 預設的行為是:「只要畫面有任何更新,我就再執行一次」。
於是發生了恐怖的連鎖反應:
1. useEffect 去抓資料。
2. 抓到資料後,呼叫 setCamps。
3. setCamps 改變了狀態,導致畫面重新渲染 (更新)。
4. 畫面更新了,又觸發了 useEffect (回到步驟 1)。
這就是鼎鼎大名的「React 無限渲染地獄」!你在一分鐘內把免費 API 額度燒光了!

✅ 正確寫法:
務必在 useEffect 的結尾加上一個空陣列 [],代表「這段邏輯只在第一次進來時執行一次,之後不管畫面怎麼變,都不要再理它了」。
*/

// 修改後的正確程式碼:
useEffect(() => {
  fetchData();
}, []); // 👈 就是這個救命的空陣列!

✅ 本章總結與除錯心法

在這堂課,你已經從一個切版工程師,晉升為可以串接資料庫的全端工程師了!

  1. 資安第一:永遠把 VITE_SUPABASE_URL 放在 .env 裡面。
  2. 非同步抓取:使用 async/await 配合 supabase.from().select() 抓資料。
  3. 體驗至上:不要讓畫面空白!用 isLoading 狀態給使用者一個溫柔的 Loading 提示。
  4. 資料轉換:用 .map() 將生硬的 JSON 資料塞入上一章做好的高級組件裡。

到目前為止,我們只在電腦的大螢幕上看這個網站。但現代人有 80% 都是用手機上網的! 下一章,我們將處理所有工程師最頭痛的問題:第十三章:RWD 響應式設計與魔術漢堡選單。我們將教你如何用 Tailwind,一秒讓你的網站適應所有手機螢幕!

解鎖完整教學內容

本章為付費內容。加入專案即可解鎖超過 5000 字的深度解析,包含 10 個以上神級 Prompt 與真實 Source Code 範例!