iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
Build on Google AI

《30天打造喵語日誌:Gemini API × Vibe Coding 實戰》系列 第 20

Day 20:【數據動態同步】從 Firestore 讀取紀錄並渲染至情緒日曆與折線圖:狀態提升與即時監聽實戰!

  • 分享至 

  • xImage
  •  

告別靜態 Mock Data,讓分析圖表真正活起來

昨天,我們成功將《喵語日誌》的對話與情緒分析結果寫入 Firestore,讓使用者的每一筆日常紀錄都能保存到雲端資料庫。

不過,先前情緒分析頁籤中的折線圖與情緒日曆,仍然使用前端寫死的 Mock Data。即使使用者在聊天室新增了心情紀錄,圖表上的內容也不會跟著改變。

因此,今天的目標就是將這些靜態資料替換成 Firestore 的真實資料,並透過 onSnapshot 建立即時同步機制,讓情緒日曆、折線圖與統計卡片都能根據使用者的實際紀錄動態更新。

為什麼需要狀態提升?

在 React 的單向資料流架構中,「喵喵對話」與「情緒分析」是兩個平行的功能模組。

  • ChatContainer 負責:接收使用者輸入、呼叫 Gemini 進行情緒分析,並將資料寫入 Firestore

  • MoodChartMoodCalendar 則負責:讀取日記資料、計算情緒統計,並渲染折線圖與月曆

如果每個子元件都自行發起 Firestore 請求,可能會造成重複讀取,也不容易在新增日記後同步更新所有畫面。

因此,這次採用「狀態提升」的方式,將日記清單統一提升到 App.tsx 管理。

整體資料流如下:

Firestore
    ↓
App.tsx 統一訂閱 diaries
    ↓
透過 Props 傳給子元件
    ↓
MoodChart / MoodCalendar 渲染資料

這樣可以讓 App.tsx 成為日記資料的單一來源,其他元件只需要專注於顯示內容,不必各自管理資料庫連線。

封裝 Firestore 即時監聽

首先,我在 src/services/firebase.ts 中封裝 subscribeUserDiaries 函式。

這個函式會根據使用者的 UID 取得專屬日記子集合,再使用 queryorderBy 按照建立時間排序,最後透過 onSnapshot 建立即時資料監聽。

import {
  collection,
  onSnapshot,
  orderBy,
  query,
} from "firebase/firestore";

export function subscribeUserDiaries(
  uid: string,
  onUpdate: (diaries: DiaryDocument[]) => void,
  onError?: (error: Error) => void
) {
  const diariesRef = collection(
    db,
    "users",
    uid,
    "diaries"
  );

  const diariesQuery = query(
    diariesRef,
    orderBy("createdAt", "asc")
  );

  return onSnapshot(
    diariesQuery,
    (snapshot) => {
      const diaries: DiaryDocument[] =
        snapshot.docs.map((doc) => ({
          id: doc.id,
          ...(doc.data() as DiaryEntryData),
          createdAt: doc.data().createdAt ?? null,
        }));

      onUpdate(diaries);
    },
    (error) => {
      console.error("❌ 讀取日誌資料失敗:", error);
      onError?.(error);
    }
  );
}

onSnapshot 的特色是,建立監聽後,只要 Firestore 中的資料發生變化,前端就會收到最新結果。

因此,當使用者新增一筆心情紀錄時,情緒日曆與折線圖不需要手動重新整理頁面,就能取得更新後的資料。

另外,onSnapshot 會回傳取消訂閱函式。當元件卸載或使用者切換時,必須呼叫這個函式,避免重複監聽與不必要的資源消耗。

在 App 中管理日記狀態

完成服務函式後,接著在 App.tsx 中建立日記資料與載入狀態。

const [diaries, setDiaries] = useState<DiaryDocument[]>([]);
const [isDiariesLoading, setIsDiariesLoading] =
  useState(true);

useEffect(() => {
  if (!user) return;

  setIsDiariesLoading(true);

  const unsubscribe = subscribeUserDiaries(
    user.uid,
    (data) => {
      setDiaries(data);
      setIsDiariesLoading(false);
    },
    () => {
      setIsDiariesLoading(false);
    }
  );

  return () => unsubscribe();
}, [user]);

這段程式會在取得 Firebase 使用者身分後,開始訂閱該使用者的日記資料。

當資料成功讀取時:

  • 使用 setDiaries 更新日記清單

  • isDiariesLoading 設為 false

  • 透過 Props 將資料傳給情緒分析相關元件

這樣一來,MoodChartMoodCalendar 都可以使用同一份資料來源,避免不同元件之間出現內容不一致的情況。

將資料轉換成圖表內容

取得 Firestore 的日記陣列後,我在 MoodChart.tsxMoodCalendar.tsx 中使用 useMemo 進行資料轉換。

情緒折線圖

折線圖會從日記資料中取出建立時間與情緒分數,再將時間戳記轉換成月、日格式,例如:

8/19
8/20

接著,再將情緒分數轉換為圖表使用的級距,繪製出使用者近期的情緒變化。

例如:

  • Great 對應較高分數

  • Okay 對應中間分數

  • Low 對應較低分數

透過折線圖,使用者可以觀察自己的情緒是否逐漸上升、下降,或是在某個區間內維持穩定。

情緒日曆

情緒日曆則會先根據日期建立資料索引,再將每筆日記對應到月曆中的日期格子。

當某一天有紀錄時,該日期會顯示:

  • 對應的色彩

  • 情緒 Emoji

  • 心情狀態

  • 點選後的日記內容

如果同一天有多筆紀錄,也可以在資料處理階段決定要顯示最後一筆、平均分數,或是將多筆資料集中呈現。

頂部統計卡片

頁面上方的統計卡片也改為根據真實資料動態計算:

  • 本月紀錄天數: 計算當月有紀錄的不重複天數

  • 平均心情指數: 即時計算所有紀錄的平均分數

這些數據都不再是固定文字,而是會隨著 Firestore 資料變化即時更新。

成果驗收:真實資料驅動圖表

完成資料串接後,我在對話框輸入以下測試內容:

今天沒做什麼特別的事

接著選取「😐 Okay」心情並送出。

Gemini 成功產生貓咪的陪伴回覆,分析結果也在背景寫入 Firestore。

https://ithelp.ithome.com.tw/upload/images/20260820/20178708IFIluzwRHs.png
▲ 使用者輸入平靜的日常文字後,貓咪產生對應的安撫與陪伴回覆。

接著切換到「情緒分析」頁籤,確認月曆與折線圖是否正確取得雲端資料。

https://ithelp.ithome.com.tw/upload/images/20260820/201787081zEZfOJhZg.png
▲ 情緒日曆根據真實紀錄,在 8/198/20 顯示動態標記。頂部統計卡片也計算出本月紀錄天數為 2 天、平均心情指數為 4.0 / 5。點選 20 號後,可以查看當天的心情與對話內容。

https://ithelp.ithome.com.tw/upload/images/20260820/20178708rDP1VpJost.png
▲ 折線圖成功讀取兩筆雲端資料,呈現從昨天 Great 到今天 Okay 的情緒變化。

最後,我重新整理網頁進行測試。

等待資料完成非同步載入後,情緒日曆、折線圖與統計數值都能正確還原,證明目前的雲端資料讀取與前端渲染流程已經成功運作。

這次實作的心得

把原本寫死的 Mock Data 替換成 Firestore 真實資料,是《喵語日誌》開發過程中的一個重要轉折點。

在這之前,畫面雖然已經完成,但資料仍然只是展示用的固定內容;完成今天的串接後,前端介面開始真正反映使用者的操作與歷史紀錄。

透過 React 狀態提升與 Firestore onSnapshot 的結合,目前的資料流變得更加清楚:

  • 對話頁面負責產生資料

  • Firestore 負責保存資料

  • App.tsx 負責統一管理資料

  • 分析元件負責轉換與呈現資料

這樣的架構不只能支援目前的圖表與日曆,也為後續的成就系統、統計分析與資料篩選留下了擴充空間。

結語

今天成功完成 Firestore 資料讀取與即時同步,讓真實的日記紀錄可以動態呈現在情緒日曆與折線圖中。

目前《喵語日誌》已經具備完整的雙向資料流:

使用者輸入
    ↓
Gemini 情緒分析
    ↓
Firestore 雲端保存
    ↓
onSnapshot 即時同步
    ↓
情緒日曆與折線圖更新

每一次與貓咪的對話,都不再只是一次性的互動,而是真正累積成一條清晰可見的情緒軌跡。

明天我們將推進至成就觸發邏輯階段,設計事件驅動的連動架構。在 Firestore 數據更新時,自動判定連續記錄或特定生活場景等解鎖條件,並結合即時彈窗動畫,用遊戲化機制為使用者的每一次記錄創造專屬的驚喜!

明天見,喵~


上一篇
Day 19:【Firestore 儲存】將對話紀錄與情緒標籤寫入雲端資料庫:設計高效又省錢的 NoSQL 文件結構!
系列文
《30天打造喵語日誌:Gemini API × Vibe Coding 實戰》20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言