第四章:效能優化:複雜歷史對話紀錄的狀態管理 (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((set, get) => ({ // 初始狀態 sessions: [], activeSessionId: null,

// 切換當前正在聊天的視窗 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()( persist( (set, get) => ({ // ... 上面寫的那一堆狀態與邏輯 ... }), { // 這是存進 localStorage 的 Key 名稱 name: 'vibe-tutor-chat-storage', // 可選:如果你不想存 activeSessionId,可以透過 partialize 過濾 partialize: (state) => ({ sessions: state.sessions }), } ) ) ```

就加這五行字!現在,無論使用者怎麼重整網頁、甚至關閉分頁明天再打開,他之前的「所有對話紀錄」與「聊天群組」,都會跟施了魔法一樣完美還原在畫面上。

透過 Zustand 的全域狀態管理與 Persist 快取,我們不依賴龐大的資料庫讀寫,就打造出了極度流暢且具備本地記憶功能的 AI 聊天室核心!這不僅大幅降低了伺服器資料庫的連線成本,也帶來了閃電般快速的使用者體驗。

解鎖完整教學內容

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