Spaces:
Sleeping
Sleeping
PreAnalysisCheck データフェッチのReact Query化
概要
PreAnalysisCheckコンポーネントにおける5つのGradio API呼び出し(scoreStep3, visScore, summary, pox, excel)を、React Queryパターンでリファクタリングした設計ドキュメント。
実装ステータス:✅ 完了
主要な変更点
実装済み
- スキーマ定義6ファイル(check-url, score-step3, vis-score, summary, pox, excel)
- APIクライアント7ファイル(各API + use-pre-analysis統合フック)
- createQueryOptions必須化(タイムアウト・キャッシュ統一管理)
- 5分タイムアウト設定(API特性に合わせてカスタマイズ)
- Zustand二重ストア(ResultStore + StateStore)
- API実行状態の追跡とプログレス表示
解決した課題
- 400行のonSubmit関数を統合フック化
- 手動キャッシュ管理 → React Query自動キャッシュ
- 個別APIの再利用性向上
- API実行状態の自動追跡
- 部分的な再試行が可能に
新しい設計
アーキテクチャパターン
個別APIフック(React Query) + 統合実行フック(Promise連鎖) + Zustand ストア
個別APIフック(useScoreStep3, useVisScore等):
- createQueryOptions必須化: タイムアウト・キャッシュ設定の統一管理
- queryClient.fetchQuery: React Queryのキャッシュ機能を活用した手動実行
- カスタムタイムアウト: 5分(デフォルト20分から変更可能)
- リトライ無効化:
retry: 0(長時間APIのため) - staleTime: Infinity(キャッシュを常に有効とする)
- gcTime: 24時間(24時間キャッシュを保持)
- 重複呼び出し防止、自動キャッシュ管理
統合実行フック(usePreAnalysis):
- 個別APIフックをPromise連鎖で順次/並列実行
- ローカルステート(useState)で各ステップのデータとエラーを管理
- 実行制御(重複実行防止、プログレス追跡)
Zustand:
- ResultStore: API結果データの永続化とコンポーネント間共有
- StateStore: API実行状態の追跡(duration, status)
- React Queryキャッシュとは独立した、アプリケーション全体で共有されるストア
このアーキテクチャにより:
- 個別APIの再利用性とテスタビリティが向上
- React Queryによる自動キャッシュ・重複防止
- 統合フックによる依存関係のある複雑な実行フローの制御
- Zustandで他コンポーネントからの確実なアクセスを保証
API呼び出しの依存関係
scoreStep3 (既存)
↓ 成功後
visScore (新規)
↓ 成功後(並列)
├─ summary (新規)
└─ pox (新規)
↓ 両方成功後
excel (新規)
シーケンス図
sequenceDiagram
participant User as ユーザー
participant Component as PreAnalysisCheck
participant Hook as usePreAnalysis
participant ApiHook as 個別APIフック
participant RQ as React Query (fetchQuery)
participant API as API Routes
participant Gradio as Gradio API
participant Store as Zustand Store
User->>Component: 競合分析ボタンクリック
Component->>Hook: execute()呼び出し
Note over Hook,Store: Step 1: ScoreStep3
Hook->>ApiHook: useScoreStep3(request)
ApiHook->>RQ: queryClient.fetchQuery(createQueryOptions(...))
RQ->>API: POST /api/rpc/gradio-proxy/score-step3
API->>Gradio: getScoreStep3
Gradio-->>API: ResultData, InputData
API-->>RQ: レスポンス
RQ-->>ApiHook: キャッシュ保存 & 返却
ApiHook-->>Hook: データ返却
Hook->>Store: setScoreDict, setCommonDict
Hook->>Store: updateApiStatus('scoreStep3', 'success')
Note over Hook,Store: Step 2: VisScore
Hook->>ApiHook: useVisScore(request)
ApiHook->>RQ: queryClient.fetchQuery(createQueryOptions(...))
RQ->>API: POST /api/rpc/gradio-proxy/vis-score
API->>Gradio: getVisScore
Gradio-->>API: ScoreTotal, UrlCategoryScores, OwnCategoryScores
API-->>RQ: レスポンス
RQ-->>ApiHook: キャッシュ保存 & 返却
ApiHook-->>Hook: データ返却
Hook->>Store: setScoreTotal, setUrlCategoryScores, setOwnCategoryScores
Hook->>Store: updateApiStatus('visScore', 'success')
Note over Hook,Store: Step 3: Summary & Pox (Promise.all並列実行)
par Summary
Hook->>ApiHook: useSummary(request)
ApiHook->>RQ: queryClient.fetchQuery(...)
RQ->>API: POST /api/rpc/gradio-proxy/summary
API->>Gradio: getSummary
Gradio-->>API: SummaryData, SummaryBaseData
API-->>RQ: レスポンス
RQ-->>ApiHook: 返却
ApiHook-->>Hook: データ返却
and Pox
Hook->>ApiHook: usePox(request)
ApiHook->>RQ: queryClient.fetchQuery(...)
RQ->>API: POST /api/rpc/gradio-proxy/pox
API->>Gradio: getPox
Gradio-->>API: SwotData (75, 78, 79), SwotTable
API-->>RQ: レスポンス
RQ-->>ApiHook: 返却
ApiHook-->>Hook: データ返却
end
Hook->>Store: setSummaryData, setSummaryBaseData, setSwotData75-79, setSwotTable
Hook->>Store: updateApiStatus('summary', 'success'), updateApiStatus('pox', 'success')
Note over Hook,Store: Step 4: Excel
Hook->>ApiHook: useExcel(request)
ApiHook->>RQ: queryClient.fetchQuery(...)
RQ->>API: POST /api/rpc/gradio-proxy/excel
API->>Gradio: getExcel
Gradio-->>API: DownloadData
API-->>RQ: レスポンス
RQ-->>ApiHook: 返却
ApiHook-->>Hook: データ返却
Hook->>Store: setDownloadData
Hook->>Store: updateApiStatus('excel', 'success')
Hook-->>Component: { isLoading, isError, apiStatuses }
Component->>User: プログレス表示 & 完了通知
ダミーモード処理
sequenceDiagram
participant Component as PreAnalysisCheck
participant API as API Routes
participant Dummy as getDummyData
participant File as public/dummy/*.json
Component->>API: POST (dummyMode: true)
API->>Dummy: getDummyData('get_vis_score.json')
Dummy->>File: ファイル読み込み
File-->>Dummy: JSONデータ
Dummy-->>API: ダミーデータ
API-->>Component: ダミーレスポンス
Note over API: Gradio APIは呼ばない
ファイル構成
スキーマ定義
schema/gradio-proxy/check-url.tsschema/gradio-proxy/score-step3.tsschema/gradio-proxy/vis-score.tsschema/gradio-proxy/summary.tsschema/gradio-proxy/pox.tsschema/gradio-proxy/excel.ts
各スキーマはzodでリクエスト・レスポンスを型定義。dummyMode、userEmail、userIdentifierを共通パラメータとして含む。
APIクライアント
api-client/gradio-proxy/check-url.tsapi-client/gradio-proxy/score-step3.tsapi-client/gradio-proxy/vis-score.tsapi-client/gradio-proxy/summary.tsapi-client/gradio-proxy/pox.tsapi-client/gradio-proxy/excel.tsapi-client/gradio-proxy/use-pre-analysis.ts(統合フック)
共通パターン:
export function useScoreStep3() {
const queryClient = useQueryClient();
return useCallback(
async (request: ScoreStep3Request): Promise<ScoreStep3Response> => {
return queryClient.fetchQuery(
createQueryOptions<ScoreStep3Response>(
{
queryKey: ['score-step3', request],
queryFn: async (): Promise<ScoreStep3Response> => {
const response = await rpcClient['gradio-proxy']['score-step3'].$post({
json: request,
});
// エラーハンドリング...
return await response.json();
},
},
5 * 60 * 1000, // 5分タイムアウト
) as FetchQueryOptions<ScoreStep3Response>,
);
},
[queryClient],
);
}
ポイント:
createQueryOptionsでタイムアウト・キャッシュ設定を統一管理- 第2引数でカスタムタイムアウト(5分 = 300,000ms)を指定
queryClient.fetchQueryで手動実行(キャッシュ活用)- 詳細なコンソールログでデバッグ可能
- 型キャスト
as FetchQueryOptions<T>で型安全性を確保
サーバールート
server/routes/gradio-proxy.tsに4エンドポイント追加:
- POST
/vis-score - POST
/summary - POST
/pox - POST
/excel
各エンドポイントはdummyMode対応し、ダミーデータまたはGradio API呼び出しを実行。
データフロー
個別APIフック → 統合フック → Zustand保存
個別APIフックの実行:
- queryClient.fetchQueryでReact Queryキャッシュを活用
- レスポンスデータを返却
統合フック(usePreAnalysis)での処理:
- 個別APIフックからデータを取得
- ローカルstate(useState)に一時保存
- 即座にZustandストアに保存:
scoreStep3→ ResultStore(scoreDict, commonDict)visScore→ ResultStore(scoreTotal, urlCategoryScores, ownCategoryScores)summary→ ResultStore(summaryData, summaryBaseData)pox→ ResultStore(swotData75, swotData78, swotData79, swotTable)excel→ ResultStore(downloadData)
- 同時にStateStoreにAPI実行状態(duration, status)を保存
コンポーネントでの利用:
- Zustandストアから結果データと実行状態を取得
- プログレス表示やエラーハンドリング
キャッシュキー設計
リクエストパラメータ全体をキャッシュキーに使用
queryKey: ['score-step3', request];
queryKey: ['vis-score', request];
queryKey: ['summary', request];
queryKey: ['pox', request];
queryKey: ['excel', request];
キャッシュ設定(api-client/query-config.ts):
{
staleTime: Infinity, // 常に新鮮なデータとして扱う
gcTime: 24 * 60 * 60 * 1000, // 24時間キャッシュを保持
retry: 0, // リトライ無効(長時間APIのため)
refetchOnWindowFocus: false, // ウィンドウフォーカス時の再フェッチ無効
refetchOnMount: false, // 再マウント時の再フェッチ無効
}
React Queryが自動的に:
- 同じリクエストでの重複呼び出し防止
- キャッシュヒット時は即座に返却
- タイムアウト処理(withTimeout関数でラップ)
エラーハンドリング
統合フックでのエラー管理
usePreAnalysisフックは各ステップのエラーを個別に管理:
const [scoreStep3Error, setScoreStep3Error] = useState<Error | null>(null);
const [visScoreError, setVisScoreError] = useState<Error | null>(null);
const [summaryError, setSummaryError] = useState<Error | null>(null);
const [poxError, setPoxError] = useState<Error | null>(null);
const [excelError, setExcelError] = useState<Error | null>(null);
エラー発生時:
- 該当ステップのエラーステートを設定
- StateStoreのAPI状態を'error'に更新
- エラーを再スロー(コンポーネント側でキャッチ可能)
再試行処理
refetch関数でエラー状態をリセットして全体を再実行:
const refetch = useCallback(() => {
// 全エラーステートをクリア
setScoreStep3Error(null);
setVisScoreError(null);
setSummaryError(null);
setPoxError(null);
setExcelError(null);
// 再実行
execute();
}, [execute]);
注意:個別APIのみの部分的な再試行は現時点では非対応。全体を再実行する必要があります。
タイムアウト処理
- createQueryOptionsで5分タイムアウトを設定(5 _ 60 _ 1000 = 300,000ms)
- withTimeout関数でPromiseをラップし、タイムアウト時はエラーをスロー
- タイムアウト時は通常のエラーとして処理される
エラーメッセージ
各APIのエラーに対して段階的なメッセージ(1/5、2/5...)を表示:
if (scoreStep3Error) return `スコア分析に失敗しました (1/5): ${scoreStep3Error.message}`;
if (visScoreError) return `ビジュアル分析に失敗しました (2/5): ${visScoreError.message}`;
if (summaryError) return `サマリー生成に失敗しました (3/5): ${summaryError.message}`;
if (poxError) return `SWOT分析に失敗しました (4/5): ${poxError.message}`;
if (excelError) return `Excelファイル生成に失敗しました (5/5): ${excelError.message}`;
使用方法
コンポーネントでの使用
import { usePreAnalysis } from '@/api-client/gradio-proxy/use-pre-analysis';
import { useResultStore } from '@/store/result';
function PreAnalysisCheck() {
const {
execute,
isLoading,
isError,
error,
apiStatuses,
refetch
} = usePreAnalysis({
ownUrl: 'https://example.com',
competitionUrls: ['https://competitor1.com'],
tempImages: {},
fvInfos: null,
cnInfo: null,
dummyMode: false,
userEmail: 'user@example.com',
userIdentifier: 'user-123',
});
// ボタンクリックで手動実行
const handleSubmit = async () => {
try {
await execute();
console.log('分析完了');
} catch (error) {
console.error('分析失敗:', error);
}
};
// プログレス計算
const progress = Object.values(apiStatuses).filter(
s => s.status === 'success'
).length;
const total = Object.keys(apiStatuses).length;
// エラー時の再試行
const handleRetry = () => {
refetch();
};
return (
<div>
<button onClick={handleSubmit} disabled={isLoading}>
{isLoading ? '分析中...' : '競合分析を実行'}
</button>
{isLoading && (
<div>進捗: {progress}/{total}</div>
)}
{isError && (
<div>
<p>エラー: {error?.message}</p>
<button onClick={handleRetry}>再試行</button>
</div>
)}
</div>
);
}
Zustandストアからの結果取得
import { useResultStore } from '@/store/result';
function ResultDisplay() {
const scoreDict = useResultStore(state => state.scoreDict);
const summaryData = useResultStore(state => state.summaryData);
return (
<div>
{scoreDict && <div>スコア: {/* 表示 */}</div>}
{summaryData && <div>サマリー: {/* 表示 */}</div>}
</div>
);
}
主要な変更点:
- 手動実行パターン:
execute()メソッドで明示的に実行(自動実行なし) - 統合フック化:400行のonSubmit関数 → usePreAnalysisフックで簡潔に
- プログレス表示:
apiStatusesで各APIの実行状態を追跡 - エラーハンドリング:
isErrorとerrorで統合されたエラー状態を取得 - 再試行機能:
refetch()で全体を再実行 - Zustand統合:結果は自動的にストアに保存され、他コンポーネントからアクセス可能
メリット
コード品質
- コード行数削減:400行のonSubmit関数を統合フック化により大幅に簡潔化
- 関心の分離:
- 個別APIフック:API呼び出しとキャッシュ管理
- 統合フック:実行フローとステート管理
- コンポーネント:UI表示とユーザーインタラクション
- テスタビリティ:個別フックを独立して単体テスト可能
- 型安全性:zodスキーマによるリクエスト/レスポンスの型定義
保守性
- 一貫したパターン:個別APIフック + 統合フックの構成は他機能と同様
- 再利用性:個別APIフック(useScoreStep3等)は他コンポーネントからも利用可能
- 拡張性:新しいAPIの追加が容易(同じパターンに従うだけ)
- 設定の一元管理:createQueryOptionsでタイムアウト・キャッシュ設定を統一
パフォーマンス
- React Queryキャッシュ:同一リクエストの重複呼び出しを自動防止
- 並列実行:summary & poxをPromise.allで同時実行
- タイムアウト制御:5分タイムアウトで長時間APIの早期エラー検知
- ローカルステート最適化:必要最小限のデータのみコンポーネントで管理
ユーザー体験
- 詳細なプログレス表示:
apiStatusesで各API(1/5、2/5...)の実行状態を追跡 - 段階的なエラーメッセージ:どのステップで失敗したか明確に表示
- 柔軟な再試行:
refetch()で簡単に全体を再実行可能 - 非同期処理の可視化:duration(実行時間)とstatus(状態)をリアルタイムで確認
開発体験
- 明確なフロー:Promise連鎖により実行順序が明示的
- デバッグ容易性:詳細なコンソールログで問題箇所を特定しやすい
- ダミーモード対応:本番APIを叩かずに開発・テスト可能
- ストア統合:Zustandで結果を保存し、コンポーネント間で簡単に共有