昨天,我們成功將《喵語日誌》的對話與情緒分析結果寫入 Firestore,讓使用者的每一筆日常紀錄都能保存到雲端資料庫。
不過,先前情緒分析頁籤中的折線圖與情緒日曆,仍然使用前端寫死的 Mock Data。即使使用者在聊天室新增了心情紀錄,圖表上的內容也不會跟著改變。
因此,今天的目標就是將這些靜態資料替換成 Firestore 的真實資料,並透過 onSnapshot 建立即時同步機制,讓情緒日曆、折線圖與統計卡片都能根據使用者的實際紀錄動態更新。
在 React 的單向資料流架構中,「喵喵對話」與「情緒分析」是兩個平行的功能模組。
ChatContainer 負責:接收使用者輸入、呼叫 Gemini 進行情緒分析,並將資料寫入 Firestore
MoodChart 與 MoodCalendar 則負責:讀取日記資料、計算情緒統計,並渲染折線圖與月曆
如果每個子元件都自行發起 Firestore 請求,可能會造成重複讀取,也不容易在新增日記後同步更新所有畫面。
因此,這次採用「狀態提升」的方式,將日記清單統一提升到 App.tsx 管理。
整體資料流如下:
Firestore
↓
App.tsx 統一訂閱 diaries
↓
透過 Props 傳給子元件
↓
MoodChart / MoodCalendar 渲染資料
這樣可以讓 App.tsx 成為日記資料的單一來源,其他元件只需要專注於顯示內容,不必各自管理資料庫連線。
首先,我在 src/services/firebase.ts 中封裝 subscribeUserDiaries 函式。
這個函式會根據使用者的 UID 取得專屬日記子集合,再使用 query 與 orderBy 按照建立時間排序,最後透過 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.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 將資料傳給情緒分析相關元件
這樣一來,MoodChart 與 MoodCalendar 都可以使用同一份資料來源,避免不同元件之間出現內容不一致的情況。
取得 Firestore 的日記陣列後,我在 MoodChart.tsx 與 MoodCalendar.tsx 中使用 useMemo 進行資料轉換。
折線圖會從日記資料中取出建立時間與情緒分數,再將時間戳記轉換成月、日格式,例如:
8/19
8/20
接著,再將情緒分數轉換為圖表使用的級距,繪製出使用者近期的情緒變化。
例如:
Great 對應較高分數
Okay 對應中間分數
Low 對應較低分數
透過折線圖,使用者可以觀察自己的情緒是否逐漸上升、下降,或是在某個區間內維持穩定。
情緒日曆則會先根據日期建立資料索引,再將每筆日記對應到月曆中的日期格子。
當某一天有紀錄時,該日期會顯示:
對應的色彩
情緒 Emoji
心情狀態
點選後的日記內容
如果同一天有多筆紀錄,也可以在資料處理階段決定要顯示最後一筆、平均分數,或是將多筆資料集中呈現。
頁面上方的統計卡片也改為根據真實資料動態計算:
本月紀錄天數: 計算當月有紀錄的不重複天數
平均心情指數: 即時計算所有紀錄的平均分數
這些數據都不再是固定文字,而是會隨著 Firestore 資料變化即時更新。
完成資料串接後,我在對話框輸入以下測試內容:
今天沒做什麼特別的事
接著選取「😐 Okay」心情並送出。
Gemini 成功產生貓咪的陪伴回覆,分析結果也在背景寫入 Firestore。

▲ 使用者輸入平靜的日常文字後,貓咪產生對應的安撫與陪伴回覆。
接著切換到「情緒分析」頁籤,確認月曆與折線圖是否正確取得雲端資料。

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

▲ 折線圖成功讀取兩筆雲端資料,呈現從昨天 Great 到今天 Okay 的情緒變化。
最後,我重新整理網頁進行測試。
等待資料完成非同步載入後,情緒日曆、折線圖與統計數值都能正確還原,證明目前的雲端資料讀取與前端渲染流程已經成功運作。
把原本寫死的 Mock Data 替換成 Firestore 真實資料,是《喵語日誌》開發過程中的一個重要轉折點。
在這之前,畫面雖然已經完成,但資料仍然只是展示用的固定內容;完成今天的串接後,前端介面開始真正反映使用者的操作與歷史紀錄。
透過 React 狀態提升與 Firestore onSnapshot 的結合,目前的資料流變得更加清楚:
對話頁面負責產生資料
Firestore 負責保存資料
App.tsx 負責統一管理資料
分析元件負責轉換與呈現資料
這樣的架構不只能支援目前的圖表與日曆,也為後續的成就系統、統計分析與資料篩選留下了擴充空間。
今天成功完成 Firestore 資料讀取與即時同步,讓真實的日記紀錄可以動態呈現在情緒日曆與折線圖中。
目前《喵語日誌》已經具備完整的雙向資料流:
使用者輸入
↓
Gemini 情緒分析
↓
Firestore 雲端保存
↓
onSnapshot 即時同步
↓
情緒日曆與折線圖更新
每一次與貓咪的對話,都不再只是一次性的互動,而是真正累積成一條清晰可見的情緒軌跡。
明天我們將推進至成就觸發邏輯階段,設計事件驅動的連動架構。在 Firestore 數據更新時,自動判定連續記錄或特定生活場景等解鎖條件,並結合即時彈窗動畫,用遊戲化機制為使用者的每一次記錄創造專屬的驚喜!
明天見,喵~