import * as React from "react"; import { QueryClient, QueryClientProvider, useQuery, useQueryClient } from "@tanstack/react-query"; import { Badge, Card, CardContent, CardDescription, CardHeader, CardTitle, Descriptions, EmptyState, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@godxjp/ui/data-display"; import { Flex, PageContainer } from "@godxjp/ui/layout"; import { PrefetchLink } from "@godxjp/ui/query"; /** * PrefetchLink · a React Router Link that fires queryClient.prefetchQuery on * hover/focus so the detail page renders instantly on click. Requires a * QueryClientProvider (and a Router) in context. The queryKey/queryFn must match * the destination page's useQuery. The `prefetchOn` prop selects the trigger: * "both" (default) | "hover" | "focus" | "none". Composed from real @godxjp/ui + * @tanstack/react-query + react-router-dom. */ const queryClient = new QueryClient(); type Invoice = { id: string; partner: string; status: "active" | "pending" | "draft" }; const invoices: Invoice[] = [ { id: "INV-0312", partner: "株式会社ベトヤ", status: "active" }, { id: "INV-0311", partner: "ハノイ物流", status: "pending" }, { id: "INV-0310", partner: "GMO決済", status: "draft" }, ]; const invoiceById = (id: string) => invoices.find((inv) => inv.id === id)!; /** Destination-page query: a deliberate delay so prefetch (vs cold fetch) is observable. */ async function fetchInvoice(id: string): Promise { await new Promise((resolve) => setTimeout(resolve, 1200)); return invoiceById(id); } const invoiceQueryKey = (id: string) => ["invoice", id] as const; /** List rows · hover/focus a 請求書番号 to prime ["invoice", id]. */ function InvoiceList() { return ( 請求書番号 取引先 状態 {invoices.map((inv) => ( fetchInvoice(inv.id)} staleTime={60_000} className="text-primary font-medium hover:underline" > {inv.id} {inv.partner} ))}
); } /** * Every prefetchOn union value, labelled. "none" never prefetches; "hover" and * "focus" prime only on that trigger; "both" (the default) primes on either. */ const triggerCases: { value: "hover" | "focus" | "both" | "none"; id: string; copy: string }[] = [ { value: "both", id: "INV-0312", copy: "既定。ホバーまたはフォーカスで先読み" }, { value: "hover", id: "INV-0311", copy: "ホバー時のみ先読み" }, { value: "focus", id: "INV-0310", copy: "キーボードのフォーカス時のみ先読み" }, { value: "none", id: "INV-0312", copy: "先読みを無効化(通常の Link)" }, ]; function TriggerMatrix() { return ( prefetchOn リンク 挙動 {triggerCases.map((c) => ( {c.value} fetchInvoice(c.id)} className="text-primary font-medium hover:underline" > {c.id} {c.copy} ))}
); } /** * Observable proof · reads the QueryClient cache directly and lists every * ["invoice", id] entry the links above have primed. Updates as you hover/focus. */ function CacheObserver() { const client = useQueryClient(); const [, forceRender] = React.useReducer((n: number) => n + 1, 0); React.useEffect(() => client.getQueryCache().subscribe(forceRender), [client]); const cached = client .getQueryCache() .getAll() .filter((q) => Array.isArray(q.queryKey) && q.queryKey[0] === "invoice"); if (cached.length === 0) { return ( ); } return ( {cached.map((q) => ( {JSON.stringify(q.queryKey)}} mono > {q.state.status === "success" ? "キャッシュ済み (cached)" : "先読み中… (fetching)"} ))} ); } /** * Destination block · the SAME queryKey/queryFn contract as the list link. * Because the link already primed ["invoice", "INV-0312"], this useQuery resolves * from cache (no skeleton) instead of paying the 1.2s fetch. */ function DestinationDetail() { const query = useQuery({ queryKey: invoiceQueryKey("INV-0312"), queryFn: () => fetchInvoice("INV-0312"), staleTime: 60_000, }); if (query.isPending) { return ; } const inv = query.data!; return ( {inv.id} {inv.partner} ); } export default function Demo() { return ( {/* The preview/isolate harness already provides a Router; PrefetchLink reads it from context. */} 請求書 一覧 請求書番号にホバーまたはフォーカスすると、その明細クエリが先読みされ次の画面が即時表示されます。 prefetchOn · トリガーの種類 既定は both。hover / focus / none で先読みのきっかけを切り替えます。 先読みキャッシュ(観測) QueryClient のキャッシュから ["invoice", id] を直接読み出して表示します。 遷移先の明細(契約) リンクと同じ queryKey/queryFn を使う useQuery。先読み済みならキャッシュから即時解決します。 ); }