読み込み中
豆知識
最終更新: 2026-05-01 API Reference: /docs/api-reference — Scalar UI で OpenAPI 3.2 を検索 API 仕様: /docs/api-reference
// ❌ WRONG: 50 items = 50 API calls!
documents.forEach((doc) => fetchLinkedDocuments(doc.id))
// ✅ CORRECT: Single batch call
const documentIds = documents.map((d) => d.id)
const { data: counts } = useDocumentLinkedCounts(documentIds)
// ❌ WRONG: Always fetches, even if panel is closed
const { data } = useDocumentLinkedDocuments(documentId)
const [isPanelOpen, setIsPanelOpen] = useState(false)
// ✅ CORRECT: Only fetch when needed
const { data } = useDocumentLinkedDocuments(documentId, {
enabled: isPanelOpen,
})
// ❌ WRONG: Data is already in /full response
const { data: doc } = useGetDocumentByIdQuery({ documentId })
const { data: attachments } = useGetDocumentAttachmentsQuery({ documentId })
const { data: expenses } = useGetDocumentExpensesQuery({ documentId })
// ✅ CORRECT: Single /full call
const { data } = useDocumentFull({
documentId,
includeAttachments: true,
includeExpenses: true,
})
// ❌ WRONG: Bypasses cache
useEffect(() => {
const fetchData = async () => {
const response = await apiClient.get(`/documents/${id}`)
setData(response.data)
}
fetchData()
}, [id])
// ✅ CORRECT: Use TanStack Query
const { data } = useDocumentFull({ documentId: id })
return useQuery({
queryKey: [QUERY_KEYS.FEATURE.ACTION, params],
queryFn: () => api.fetchData(params),
enabled: !!requiredParam, // REQUIRED
staleTime: 60_000, // REQUIRED (30s minimum)
gcTime: 5 * 60_000, // RECOMMENDED
refetchOnWindowFocus: false, // REQUIRED
refetchOnMount: false, // RECOMMENDED
})
| Data Type | staleTime | Reason |
|---|---|---|
| Static/Config | 10-30 min | Rarely changes |
| Reference Data | 5-10 min | Changes infrequently |
| List Data | 1-2 min | Changes with operations |
| Individual Items | 30-60 sec | May be edited |
| Real-time | 15-30 sec | Notifications, stats |
| AI/Expensive | 5 min | Avoid re-running |
The backend provides a consolidated endpoint that returns all document-related data in a single request.
GET /api/documents/:id/full?include_linked_docs=true&include_attachments=true&include_expenses=true&include_dock=true
| Pattern | API Calls | Latency |
|---|---|---|
| Before (separate calls) | 5-6 calls | ~2700ms |
| After (/full endpoint) | 1 call | ~180ms |
| Improvement | -83% | -93% |
{
document: {
document: { /* actual document */ },
documentVersions: { /* version info */ }
},
linkedDocs: { linkedDocuments: [] },
attachments: [],
expenses: [],
dockData: {
currentDocument: { id, workflowStage, progress },
claimsDocuments: [{ id, totalAmount, attachmentCount }],
totalReimbursementAmount: number
},
meta: { includedFields: [], fetchTimeMs: number }
}
import { useDocumentFull } from "@/features/document/hooks/use-document-full"
const { data, isLoading } = useDocumentFull({
documentId,
includeLinkedDocs: true,
includeAttachments: true,
includeExpenses: true,
includeDock: true,
enabled: !!documentId,
})
// Access data
const document = data?.document?.document
const expenses = data?.expenses
const dockData = data?.dockData
The hook automatically pre-populates caches:
// After /full, these caches are populated:
;["getDocumentById", documentId][("getDocumentExpenses", documentId)][
("getDocumentAttachments", documentId)
]
// Other components using individual hooks get cached data!
const { data } = useGetDocumentExpensesQuery({ documentId }) // NO new call!
Use batch APIs - never fetch individual details per item.
function DocumentList() {
const { data: documents } = useDocumentListQuery({ page: 1, limit: 50 })
const documentIds = documents?.items?.map(d => d.id) || []
// Single batch call for all linked counts
const { data: linkedCounts } = useDocumentLinkedCounts(documentIds)
return (
<table>
{documents?.items?.map(doc => (
<tr key={doc.id}>
<td>{doc.name}</td>
<td>{linkedCounts?.data?.[doc.id]?.count || 0}</td>
</tr>
))}
</table>
)
}
Fetch full details only when viewing, use enabled for panels.
function DocumentDetail({ documentId }) {
const [isLinkPanelOpen, setIsLinkPanelOpen] = useState(false)
// Always fetch - user is viewing
const { data: document } = useDocumentDetailQuery(documentId)
// Only fetch when panel opens
const { data: linkedDocs } = useDocumentLinkedDocuments(documentId, {
enabled: isLinkPanelOpen
})
return (
<div>
<DocumentViewer data={document} />
<button onClick={() => setIsLinkPanelOpen(true)}>View Links</button>
{isLinkPanelOpen && <LinkPanel data={linkedDocs} />}
</div>
)
}
| Endpoint | Purpose |
|---|---|
GET /documents/linked-counts?ids=a,b,c |
Linked counts for multiple docs |
POST /transactions/batch-analyze |
AI analysis for multiple transactions |
GET /clients?ids=a,b,c |
Multiple clients by ID |
export const useDocumentLinkedCounts = (documentIds: string[]) => {
// Deduplicate and sort for consistent cache key
const uniqueSortedIds = [...new Set(documentIds)].sort()
return useQuery({
queryKey: ["document-links", "counts", uniqueSortedIds],
queryFn: () => documentLinksApi.getLinkedCounts(uniqueSortedIds),
enabled: uniqueSortedIds.length > 0,
staleTime: 60_000,
refetchOnWindowFocus: false,
refetchOnMount: false,
})
}
If you need to:
Ask Backend Team to create a batch endpoint.
// ✅ Good: Clear hierarchy
;["documents", "list", { status: "active", page: 1 }][
("documents", "detail", documentId)
][("documents", "detail", documentId, "attachments")][
// ❌ Bad: Inconsistent
("getDocuments", params)
][("document-attachments", documentId)]
export const DOCUMENT_QUERY_KEYS = {
all: ["documents"] as const,
lists: () => [...DOCUMENT_QUERY_KEYS.all, "list"] as const,
list: (params) => [...DOCUMENT_QUERY_KEYS.lists(), params] as const,
details: () => [...DOCUMENT_QUERY_KEYS.all, "detail"] as const,
detail: (id) => [...DOCUMENT_QUERY_KEYS.details(), id] as const,
attachments: (id) =>
[...DOCUMENT_QUERY_KEYS.detail(id), "attachments"] as const,
}
// Usage
useQuery({ queryKey: DOCUMENT_QUERY_KEYS.detail(documentId) })
// Invalidation
queryClient.invalidateQueries({
queryKey: DOCUMENT_QUERY_KEYS.detail(documentId),
})
// ❌ Bad: Different key order = cache miss
useQuery({ queryKey: ["docs", { status: "active", page: 1 }] })
useQuery({ queryKey: ["docs", { page: 1, status: "active" }] }) // Different!
// ✅ Good: Sort parameters
const normalizedParams = Object.keys(params)
.sort()
.reduce((acc, key) => ({ ...acc, [key]: params[key] }), {})
Before submitting PR with API calls:
enabled used for conditional fetchingstaleTime set (minimum 30s)refetchOnWindowFocus: falserefetchOnMount: false where appropriate| Scenario | Solution |
|---|---|
| Need counts for 50 items | Batch API: GET /documents/linked-counts?ids=... |
| Panel with document details | enabled: isPanelOpen |
| Same data needed by multiple components | Same query key, let TanStack Query dedupe |
| User might never need this data | enabled: false until needed |
| Data changes rarely | staleTime: 10 * 60 * 1000 |
| Data changes often | staleTime: 30 * 1000 |
| AI/expensive operation | staleTime: 5 * 60 * 1000 |
| File | Purpose |
|---|---|
src/features/document/hooks/use-document-full.ts |
Consolidated fetch hook |
src/features/document/components/hydrate-document-data.tsx |
Document viewer data layer |
src/lib/query-config.ts |
TanStack Query cache config |
/docs/api-reference (このフロントエンドの dev server だけで閲覧可。バックエンド起動は不要)/api-docs (起動中のみ)api-request タグで起票読み込み中
豆知識