tanstack-data-loading について
tanstack-data-loading は、TanStack React Start プロジェクトのデータ読み込み実装とレビューを支援するコーディングスキルです。プロジェクト構成とバージョンを確認し、ローダー、SSR、コンポーネント、ミューテーションのルールを適用します。
主な機能
- 01実装、監査、更新の各モード
- 02アプリ構成の検出とローカルキャッシュ
- 03TanStack 依存バージョンの確認
- 04ローダーの並行処理とエラー処理の指針
- 05SSR、コンポーネント、ミューテーションのルール
活用シーン
- 01ルートローダーのリクエストウォーターフォール監査
- 02アプリのデータ読み込み検出記録の更新
- 03検出したバックエンドに沿ったサーバー関数の実装
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ draekien/ skills --skill tanstack-data-loadingインストール後、Claude Code で/tanstack-data-loadingを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/audit.mdbackend-database.mdbackend-service.mdversions.mdscripts/discovery.py詳細
TanStack データローディング
最初の引数でモードを選択します。モード引数がない場合は、内容から判断します。監査、ウォーターフォールの発見、既存コードのレビューを依頼された場合は audit、再発見やキャッシュの無効化を依頼された場合は refresh、それ以外は書き込みです。
audit [--fix] [path]— 開始前に references/audit.md を読み、pathに対して実行します。refresh [path]—pathを含むアプリについて Discovery を再実行し、結果を書き込んで終了します。- 書き込み — 指定されている場合は
path内で、以下のルールを、書き込みまたはレビュー対象のコードに適用します。
どのモードも Discovery から始めます。ルールは記録内容に応じて分岐します。バックエンドの構成を推測すると、誤ったサーバー関数のルールと誤った書き込みルールを適用することになります。
利用可能なスクリプト
scripts/discovery.py—<repo-root>/.draekien/tanstack-data-loading/discovery.jsonにある Discovery キャッシュをreadおよびwriteします。アプリのルートごとに Discovery エントリを 1 つ保持します。versionsはインストール済みの@tanstackのバージョンを表示します。終了コードは--helpに記載されています。
検出
アプリのルート。 path(または現在のディレクトリ)の最も近い祖先のうち、package.json が @tanstack/react-start に依存しているディレクトリです。一致するものがない場合、またはタスクが複数のアプリにまたがる場合は、どのアプリを対象とするかユーザーに確認してください。
読み取り。 このスキルのディレクトリからスクリプトを実行し、リポジトリのルートを基準としたアプリのルートを渡します(単一パッケージのリポジトリでは .)。
uv run scripts/discovery.py read --app apps/web --repo <repo-root>
| 終了コード | 意味 | 次の手順 |
|---|---|---|
| 0 | 最新の検出エントリが出力された | それを使用し、名前が挙げられているルールの代わりに versionNotes を適用します。refresh では、検出して書き込みます。 |
| 4 | 検出エントリがない | 検出して書き込みます。 |
| 5 | スクリプトの最大有効期間を超えている、またはインストール済みの @tanstack のバージョンが変更された | 検出して書き込みます。 |
| 3 | キャッシュファイルまたはインストール済みパッケージのマニフェストを読み取れない | 標準エラー出力を確認します。キャッシュファイルの場合はユーザーに伝え、すべてのアプリの検出エントリが格納されているため、ユーザーの同意を得てから削除します。マニフェストの場合は、依存関係を再インストールするようユーザーに依頼します。 |
| 2 | 使用方法のエラー | 標準エラー出力を確認し、呼び出しを修正して再実行します。 |
検出。 各フィールドの値はコードから確認します。
| フィールド | 値 | 確認方法 |
|---|---|---|
backend | database、service、both | createServerFn のハンドラーを読みます。database:データベースクライアントに直接クエリを実行します。service:HTTP 経由で別のサービスを呼び出します。both:両方を行うものがあります。 |
pendingUi | route-level、per-section | ルーターで defaultPendingComponent が設定されている場合は route-level です。それ以外の場合は、ルートレベルの保留コンポーネントにするか、Suspense セクションのみにするかをユーザーに確認します。ユーザーに確認できない場合は per-section を記録し、その旨を伝えます。 |
verify | コマンドのリスト | リポジトリの package.json の scripts にある lint、型チェック、テストのコマンドです。 |
versionNotes | 文字列のリスト | uv run scripts/discovery.py versions --app <app-root> --repo <repo-root> を実行し、Versions と照合します。references/versions.md を参照するよう指示された場合は、確認結果を記録します。該当する変更がない場合は checked: no changes と記録します。それ以外は空にします。versions が {} を出力した場合は、続行する前に依存関係をインストールするようユーザーに依頼します。 |
書き込み。 スクリプトは discoveredAt とインストール済みの versions を追加し、不明な値を拒否します。
uv run scripts/discovery.py write --app apps/web --repo <repo-root> <<'EOF'
{"backend": "database", "pendingUi": "per-section", "verify": ["pnpm check", "pnpm typecheck", "pnpm test"], "versionNotes": []}
EOF
リポジトリで初めて書き込む場合。 終了コード 6 は .draekien/ が存在しないことを示します。作成する前にユーザーに確認してください。.draekien/tanstack-data-loading/discovery.json がまだ存在しない場合は、.draekien/tanstack-data-loading/ を .gitignore に追加するかどうかをユーザーに確認してください。検出エントリには、開発者個人のローカルな検出結果が格納されます。ユーザーが .draekien/ の作成に同意しない場合は、このタスクに限って検出したフィールドを保持し、検出キャッシュを書き込まなかったことを伝えます。
アプリについて read または write が終了コード 0 で終了したとき、あるいはユーザーがキャッシュを拒否し、すべてのフィールドが判明しているときに、検出は完了です。
バックエンドのルール。 サーバー関数、ミューテーション、書き込み処理を作成または監査する前に、backend に対応するリファレンスを読みます。
database— references/backend-database.md。service— references/backend-service.md。both— 両方を読みます。それぞれに該当するハンドラーを持つサーバー関数に各ルールを適用します。両方を行うハンドラーには、各処理に該当するリファレンスを適用します。
バージョン
ルールは @tanstack/react-query 5.104.2、@tanstack/react-router 1.170.44、@tanstack/react-start 1.169.1 を基準に確認されています。以下の API を使用するルールを適用する前に、インストール済みのバージョン(検出エントリの versions、または versions コマンド)を最低バージョンと比較し、インストール済みバージョンがそれより低い場合は代替手段を使用してください。
| ルールで使用するもの | 必要なバージョン | 最低バージョン未満の場合 |
|---|---|---|
queryClient.query、queryClient.infiniteQuery | @tanstack/react-query 5.102.0 | fetchQuery、prefetchQuery、ensureQueryData およびそれらの無限クエリ版を使用します。これらはそのバージョンでは非推奨ではありません |
staleTime: "static" | @tanstack/react-query 5.79.0 | ensureQueryData(options) を使用します。ローダー内で同じ動作をします |
import { noop } from "@tanstack/react-query" | @tanstack/react-query 5.76.0 | .catch(() => {}) を使用します |
ミューテーションの scope: { id } | @tanstack/react-query 5.32.0 | isPending の間はトリガーを無効にします |
createServerFn().validator(...) | @tanstack/react-start 1.168.25 | .inputValidator(...) を使用します。1.169.0 以降のバージョンでは .validator(...) のみ受け付けます |
クライアントからのインポートを禁止する *.server.ts | @tanstack/react-start 1.166.8 | このサフィックスは命名規則にすぎず、インポートをブロックするものはありません |
インストール済みのメジャーバージョンが確認済みのものより新しい場合、またはルールで使用する API がインストール済みの型定義にない場合で、検出エントリに versionNotes がないときは、ルールを適用する前に references/versions.md を読んでください。
ローダー
対象:アプリルート配下のすべてのファイルルートの loader オプションです。
ルートがその読み込みなしではレンダリングできない、または失敗時に別のものをレンダリングする必要がある場合、その読み込みはクリティカルです。メインコンテンツ、存在確認(404)、認可(403)、リダイレクト、利用制限のゲート、フォームのデフォルト値が該当します。それ以外の読み込みはすべて未待機です。
-
クリティカルな読み込み:
await context.queryClient.query(options)。ローダーで待機すると、レンダリング前に404または403がnotFoundComponentまたはerrorComponentに送られます。エラーはそのまま処理先に届くようにするか、notFound()をスローしてください。 -
未待機の読み込み。トグルの2つ目の期間など、非表示のバリエーションも含みます:
void context.queryClient.query(options).catch(noop)。.catch(noop)がないと、サーバー上で拒否が未処理になります。コンポーネントのuseSuspenseQueryは引き続きエラーを再スローし、エラーバウンダリーに渡します。 -
未待機の読み込みはすべて、最初の
awaitより前に開始してください。awaitの後に開始する読み込みはウォーターフォールになります。 -
独立したクリティカルな読み込み:
await Promise.all([...])を1つ使用します。✗ await context.queryClient.query(orderQueryOptions(id)) await context.queryClient.query(customerQueryOptions(customerId)) ✓ await Promise.all([ context.queryClient.query(orderQueryOptions(id)), context.queryClient.query(customerQueryOptions(customerId)), ]) -
読み込み
bが読み込みaの結果を必要とする場合、これは取り除けないウォーターフォールです。abローダー クリティカル クリティカル const x = await query(a)の後にawait query(b(x))クリティカル 未待機 const x = await query(a)の後にvoid query(b(x)).catch(noop):awaitの後に開始する唯一の未待機読み込みです未待機 未待機 void query(a).then((x) => query(b(x))).catch(noop)。最初のawaitより前に開始します -
ローダーとコンポーネントでは、同じ
queryOptionsオブジェクトを使用します。コンポーネントで別のキーを使うと、データを2回取得します。 -
サインアウト中の訪問者が開けるルートでは、セッションを必要とする読み込みはサインイン済みの分岐内でのみ開始してください。そうしないと、サインアウト中の訪問のたびにエラーになる呼び出しが実行されます。
-
非推奨の
QueryClientメソッドは置き換えてください。非推奨 置き換え fetchQuery(options)query(options)prefetchQuery(options)void query(options).catch(noop)ensureQueryData(options)query({ ...options, staleTime: "static" })。同じ動作になります。ミューテーションがキーを無効化する場合は、代わりにstaleTime: Infinityを使用してください。後続のローダー実行で"static"を使うと、無効化されたキャッシュデータが返されるため、ルートは古いデータに基づいて分岐、リダイレクト、またはゲートを決定します。一方、Infinityは先に再取得します。1回のquery()呼び出しに渡したstaleTimeはクエリに保持されません。無効化によってマウント済みのページが再取得されるかどうかは、コンポーネントのuseSuspenseQueryのstaleTimeによって決まりますfetchInfiniteQuery(options)infiniteQuery(options)prefetchInfiniteQuery(options)void infiniteQuery(options).catch(noop)ensureInfiniteQueryData(options)ensureQueryDataと同じstaleTimeの選択でinfiniteQuery(...) -
読み込みをスクロールや操作まで遅らせないでください。また、ルーターの
defaultPreload: "intent"以外に、ホバー時のプリフェッチを追加しないでください。未待機の読み込みはページをブロックせずにすでにストリーミングされます。遅延させるとレンダリング後にリクエストが追加されます。また、遅延させた場合にuseSuspenseQueryがどう動作するかを定義した TanStack のドキュメントはありません。
ストリーミングと SSR
対象:ルーターファイルと各ルートの ssr オプション。
- ここでのルールは、ルーターファイルが
@tanstack/react-router-ssr-queryのsetupRouterSsrQueryIntegrationを呼び出す場合にのみ適用されます。呼び出していない場合は、それを最初の指摘事項として報告します。 - await されていない読み取りはサーバーで実行され、サーバーのレスポンスをブロックせずにクライアントへストリーミングされます。クリティカルな読み取りは、解決するまでレスポンスをブロックします。
useSuspenseQueryとローダーの読み取りはサーバーで実行されます。useQueryは実行されません。そのデータはハイドレーション後に読み込まれるため、useQueryで読み取るルートコンポーネントはウォーターフォールになります。ssr: "data-only"はサーバーでbeforeLoadとloaderを実行し、コンポーネントはクライアントでのみレンダリングします。ssr: "data-only"で await されていない読み取りがストリーミングされることを前提にせず、修正策として推奨しないでください。その動作を明記した公式ドキュメントはありません。タスクがこの動作に依存する場合は、実行中のアプリで確認してください。- ルートコードの分割は設定しないでください。Start では常に有効で、ローダーはメインチャンクに残ります。
コンポーネント
対象:サスペンスを使ってクエリデータを読み取るコンポーネント。
-
1つのコンポーネント内にサスペンスクエリが2つ以上ある場合は、
useSuspenseQueriesを1回呼び出します。1つのコンポーネント内でuseSuspenseQueryを個別に呼び出すとウォーターフォールになります。2つ目のクエリは1つ目が解決した後に開始されます。✗ const { data: order } = useSuspenseQuery(orderQueryOptions(id)) const { data: notes } = useSuspenseQuery(notesQueryOptions(id)) ✓ const [{ data: order }, { data: notes }] = useSuspenseQueries({ queries: [orderQueryOptions(id), notesQueryOptions(id)], }) -
ルートで把握できるクエリでサスペンドする子コンポーネントがある場合は、その読み取りをルートローダーで開始します。そうでない場合、子のリクエストは親のレンダリング後にのみ開始されます。
-
await されていない読み取りは、それぞれ専用の Suspense セクション 内でレンダリングします。エラー UI に再試行機能がある場合は、
QueryErrorResetBoundaryの内側で、@tanstack/react-routerの<CatchBoundary getResetKey={...} errorComponent={...}>を<React.Suspense fallback={...}>の周囲に配置します。セクションのストリーミング中もページの残りの部分はレンダリングされ、エラーはそのセクション内にとどまります。クリティカルな読み取りには Suspense セクションは必要ありません。 -
同じルート上でクエリキーが変わる場合(検索パラメーター、タブ、ページ、期間など)は、変更を
React.startTransition内で行います。useSuspenseQueryにplaceholderDataはありません。トランジションを使わないと、React はコンテンツをフォールバックに置き換えます。 -
pendingUi: route-level:ルーターはdefaultPendingComponentを設定し、アプリのシェルレイアウト内にレンダリングします。defaultPendingMs(デフォルトは1000)の経過後にレンダリングされ、少なくともdefaultPendingMinMs(デフォルトは500)の間表示されます。pendingUi: per-section:defaultPendingComponentは追加しません。ユーザーがこの設定を選択済みで、ルーターにdefaultPendingComponentが設定されている場合は、削除を依頼されるまでそのままにします。
ミューテーション
対象:すべての useMutation。
-
サーバー関数は保存したエンティティを返します。
onSuccessでqueryClient.setQueryData(key, saved)を使って書き込みます。 -
onSuccessでは変更によって影響を受けるキー(リスト、件数、合計)のみを無効化し、無効化の完了を待ちません。onSuccessから返された、またはonSuccess内で await されたプロミスがあると、無効化されたすべてのクエリの再取得が完了するまでisPendingはtrueのままです。✗ onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: orderKeys.all }) } ✓ onSuccess: (saved) => { queryClient.setQueryData(orderKeys.detail(saved.id), saved) void queryClient.invalidateQueries({ queryKey: orderKeys.lists() }) } -
書き込んだキーは無効化しないでください。無効化すると再取得されます。そのキーを含むプレフィックスを無効化するには、
predicateで書き込んだキーを除外します。void queryClient.invalidateQueries({ queryKey: orderKeys.all, predicate: (query) => query.queryKey.length !== written.length || query.queryKey.some((part, index) => part !== written[index]), }) -
競合する可能性のある1つのエンティティへの保存(自動保存、短時間に繰り返す切り替えなど)では、エンティティを表す文字列を
scope: { id }に設定します。たとえば`order-${orderId}`のようにします。同じscope.idを持つミューテーションは、呼び出し順に1つずつ実行されます。保存のたびに再取得しても順序は保証されません。レスポンスが逆の順序で届く可能性があります。 -
ユーザーが参照できるデータを変更するミューテーション(アカウントやワークスペースの切り替えなど)では、次の画面に前のアカウントのデータが表示されないよう、無効化の完了を待ちます。それ以外のミューテーションには、上記のルールを適用します。
サーバー関数
対象:すべての createServerFn。
createServerFnのエクスポートは*.functions.tsに配置します。クライアントコードからインポートできます。ビルド時に各ハンドラーがリクエスト用スタブに置き換えられます。サーバー専用のヘルパーは*.server.tsに配置します。このファイルはインポート保護により、クライアントコードからのインポートがブロックされます。- 入力を検証します:
.validator(z.object({ ... }))。 - クライアントからの各呼び出しは、固定コストの HTTP リクエストです。コストにはリクエスト自体に加え、実行されるすべてのミドルウェアが含まれます。呼び出しのバッチ化も重複排除も行われません。3 つのサーバー関数に対する
Promise.allは、3 件の並行リクエストを送信します。同一の呼び出しを 2 回行うと、2 件のリクエストが送信されます。同一の読み取りは、サーバー関数をqueryFnからのみ呼び出してクエリーキャッシュ経由で重複排除します。 - ページ上のすべての読み取りを 1 つのサーバー関数にまとめないでください。ページが最も遅い部分を待つことになり、Suspense のセクションが個別にストリーミングされなくなり、ミューテーションのたびにすべてのデータが再取得されます。
- ミドルウェアは、それを使用するすべてのサーバー関数の呼び出しごとに実行されます。共有ミドルウェアでは、その関数すべてが読み取る値だけを計算します。一部の関数だけが読み取る値は、個別のミドルウェアに配置します。
- 1 つのデータベース接続で送信されるステートメントは、1 つのトランザクション内のすべてのステートメントを含め、1 件ずつ実行されます。それらに対する
Promise.allを使っても時間は短縮されません。代わりにステートメントの数を減らしてください。 - 「Discovery」の「Backend rules」で指定されたバックエンドのリファレンスを、すべてのサーバー関数に適用します。
注意点
useSuspenseQueriesは無限クエリーを受け付けません。同じコンポーネント内に無限クエリーがある場合、ほかのクエリーが解決してから実行が始まります。ローダーで実行してください。useSuspenseQueriesに切り替えた後は、@tanstack/react-queryのモックを使用しているすべてのテストで、useSuspenseQueriesをエクスポートする必要があります。- キャッシュされた行に対する
toBeは失敗します。構造共有によって同一オブジェクトではなく、等しいコピーが返されるためです。toEqualを使用してください。 - ローダーが読み取りを待機するようになった後、書き込み直後にその読み取りが 404 を返すと(読み取りモデルの更新が非同期のバックエンドの場合)、Suspense のセクション内にエラーが表示されるのではなく、ルートのエラーコンポーネントが表示されます。