第四章:パフォーマンス最適化:複雑な履歴会話記録の状態管理(Zustand)とキャッシュメカニズム

AIチャットルームが前章のような単純な「一問一答」ではなく、コンテキストを記憶し、異なる「チャットセッション」を切り替える必要がある場合、単純なuseStateはすぐに破綻します。

すべての会話データ、ロード状態、履歴記録を1つのComponentに詰め込むと、1つの状態が変更されるだけでページ全体が激しく再レンダリング(Re-render)され、タイプライター効果の遅延さえ引き起こす可能性があります。

本章では、React界で現在最も人気のある軽量グローバル状態管理ライブラリZustandを使用して、高性能な会話記録管理システムを構築する方法を紹介します。

なぜReduxやContext APIを使わないのか?

過去において、このようなコンポーネントを跨ぐ状態管理には通常2つの選択肢がありました:

  • Context API:深刻な「レンダリング地獄」を引き起こしやすい。Providerの値が変更されると、内部のすべての子コンポーネントが無条件に再レンダリングされ、Streamingのように10ミリ秒ごとに状態を更新するシナリオではパフォーマンス上の災難となります。
  • Redux:重すぎる。大量のAction、Reducer、Dispatchのボイラープレートコードを書く必要があり、Vibe Codingの開発速度を大幅に低下させます。

Zustandの利点は:極めてシンプル、Context Providerに依存せず、非常に精密な「セレクター(Selectors)」をサポートして不必要なレンダリングを防ぐことです。

ステップ1:状態ストア(Store)の定義

まずZustandをインストール:npm install zustand

src/store/useChatStore.tsファイルを作成します。このStoreがチャットルームの頭脳となります:

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<ChatState>((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 };
    })
  })),
}))

ステップ2:コンポーネントでの状態の効率的な使用

Storeが準備できたので、UIでどのように使用するかを見てみましょう。Zustandの最も強力な点は、「必要なものだけを取得できる」ことであり、他の部分が変更されてもこのコンポーネントは再レンダリングされません。

例えば、「履歴リスト」を表示する左サイドバーがある場合:

"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="w-72 bg-gray-900 text-white h-screen p-4 flex flex-col gap-4 shadow-2xl z-10">
      <button 
        onClick={createNewSession}
        className="flex items-center gap-2 bg-white/10 hover:bg-white/20 text-white font-bold py-3 px-4 rounded-xl transition-all border border-white/5"
      >
        <PlusCircle className="w-5 h-5" /> 
        新しい会話
      </button>
      
      <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.tspersistを少し追加するだけで、ブラウザのlocalStorageにデータを自動保存できます:

import { create } from 'zustand'
import { persist } from 'zustand/middleware'

export const useChatStore = create<ChatState>()(
  persist(
    (set, get) => ({
      // ... 上記の状態とロジック ...
    }),
    {
      // localStorageに保存する際のキー名
      name: 'vibe-tutor-chat-storage', 
      // オプション:activeSessionIdを保存したくない場合、partializeでフィルタリング可能
      partialize: (state) => ({ sessions: state.sessions }),
    }
  )
)

たったこれだけのコードで!これで、ユーザーがページを更新したり、タブを閉じて翌日再度開いたりしても、以前の「すべての会話記録」と「チャットグループ」が魔法のように画面に復元されます。

Zustandのグローバル状態管理とPersistキャッシュにより、大規模なデータベースの読み書きに依存することなく、非常にスムーズでローカル記憶機能を備えたAIチャットルームのコアを構築できました!これはサーバーデータベースの接続コストを大幅に削減するだけでなく、稲妻のように速いユーザー体験を提供します。

完全なチュートリアルをロック解除

このチャプターは有料コンテンツです。プロジェクトに参加して、10以上の神レベルのPromptや実際のソースコード例を含む、5000字以上の深い分析をロック解除してください!