第四章:效能優化:複雜歷史對話紀錄的狀態管理 (Zustand) 與快取機制
當你的 AI 聊天室不只是像上一章那樣「一問一答」,而是需要記住上下文、並且支援切換不同的「聊天群組 (Chat Sessions)」時,原本簡單的 `useState` 就會瞬間崩潰。
如果把所有的對話資料、載入狀態、歷史紀錄全部塞在一個 Component 裡,只要一個狀態改變,整個頁面就會瘋狂重新渲染 (Re-render),甚至導致你的打字機特效卡頓。
本章將帶你使用 React 圈目前最受歡迎的輕量級全域狀態管理庫 Zustand,來打造高效能的對話紀錄管理系統。
為什麼不用 Redux 或 Context API?
在過去,面對這種跨元件的狀態,我們通常有兩個選擇:
- Context API:容易引發嚴重的「渲染地獄」。當 Provider 的值改變時,裡面所有的子元件都會無腦重新渲染,對 Streaming 這種每 10 毫秒就更新一次狀態的情境來說是效能災難。
- Redux:太重了。你需要寫一堆 Action, Reducer, Dispatch 樣板程式碼,大幅降低 Vibe Coding 的開發速度。
Zustand 的優勢在於:極簡、不依賴 Context Provider、而且支援非常精準的「選取器 (Selectors)」以避免不必要的渲染。
步驟一:定義狀態商店 (Store)
首先安裝 Zustand:`npm install zustand`
建立一個 `src/store/useChatStore.ts` 檔案。這個 Store 就是我們聊天室的大腦:
```typescript import { create } from 'zustand'
export type Message = { id: string role: 'user' | 'assistant' content: string }
export type ChatSession = { id: string title: string messages: Message[] }
interface ChatState { sessions: ChatSession[] activeSessionId: string | null
// 動作 (Actions) setActiveSession: (id: string) => void addMessage: (sessionId: string, msg: Message) => void updateLastMessage: (sessionId: string, chunk: string) => void createNewSession: () => string }
export const useChatStore = create
// 切換當前正在聊天的視窗 setActiveSession: (id) => set({ activeSessionId: id }),
// 開啟新對話 createNewSession: () => { const newId = crypto.randomUUID() set((state) => ({ // 將新對話推到陣列最前面 sessions: [{ id: newId, title: '新的探索', messages: [] }, ...state.sessions], activeSessionId: newId })) return newId },
// 增加完整的新訊息 addMessage: (sessionId, msg) => set((state) => ({ sessions: state.sessions.map(session => session.id === sessionId ? { ...session, messages: [...session.messages, msg] } : session ) })),
// 更新最後一則訊息 (這專門為了 Streaming 打字機設計的效能最佳化寫法!) updateLastMessage: (sessionId, chunk) => set((state) => ({ sessions: state.sessions.map(session => { if (session.id !== sessionId) return session;
const lastMsg = session.messages[session.messages.length - 1];
if (!lastMsg || lastMsg.role !== 'assistant') return session;
// 複製一份陣列,然後把新的字串片段「黏」到最後一則訊息的內容後面
const newMessages = [...session.messages];
newMessages[newMessages.length - 1] = {
...lastMsg,
content: lastMsg.content + chunk
};
return { ...session, messages: newMessages };
})
})), })) ```
步驟二:在元件中精準取用狀態
Store 建好了,我們來看看如何在 UI 裡面使用它。Zustand 最強大的地方在於,你可以「只拿你需要的東西」,這樣當其他東西改變時,這個元件就不會被牽連而重新渲染。
例如,我們有一個左側邊欄負責顯示「歷史紀錄列表」:
```tsx "use client" import { useChatStore } from "@/store/useChatStore" import { PlusCircle, MessageSquare } from "lucide-react"
export function Sidebar() { // 這裡我們精準提取需要的狀態和動作 // 這樣當 messages 更新打字機動畫時,Sidebar 不會跟著重新渲染! const sessions = useChatStore(state => state.sessions) const activeSessionId = useChatStore(state => state.activeSessionId) const setActiveSession = useChatStore(state => state.setActiveSession) const createNewSession = useChatStore(state => state.createNewSession)
return (
<div className="flex-1 overflow-y-auto mt-2 space-y-2 pr-2 custom-scrollbar">
{sessions.map(s => (
<div
key={s.id}
onClick={() => setActiveSession(s.id)}
className={\`flex items-center gap-3 p-3 rounded-xl cursor-pointer transition-all \${
activeSessionId === s.id
? 'bg-blue-600 font-bold shadow-lg shadow-blue-500/30'
: 'hover:bg-white/5 text-white/70'
}\`}
>
<MessageSquare className="w-4 h-4 opacity-50" />
<span className="truncate flex-1">{s.title}</span>
</div>
))}
</div>
</div>
) } ```
這段程式碼的巧妙之處在於:當你在右邊的對話框裡面享受 Streaming 打字機特效時(觸發 `updateLastMessage`),左側的 Sidebar 完全不會重新渲染!
結合 localStorage 做到資料持久化 (Persist)
更狂的是,Zustand 內建了強大的 Middleware。 想像一下,使用者正在進行一段極具價值的 AI 對話,不小心按到了 F5 重新整理,如果聊天紀錄全都不見了,他一定氣炸。 我們只要在 `useChatStore.ts` 中稍微包一層 `persist`,就能全自動把資料存進瀏覽器的 localStorage:
```typescript import { create } from 'zustand' import { persist } from 'zustand/middleware'
export const useChatStore = create
就加這五行字!現在,無論使用者怎麼重整網頁、甚至關閉分頁明天再打開,他之前的「所有對話紀錄」與「聊天群組」,都會跟施了魔法一樣完美還原在畫面上。
透過 Zustand 的全域狀態管理與 Persist 快取,我們不依賴龐大的資料庫讀寫,就打造出了極度流暢且具備本地記憶功能的 AI 聊天室核心!這不僅大幅降低了伺服器資料庫的連線成本,也帶來了閃電般快速的使用者體驗。