mcpField
T
tanstack-data-loading

tanstack-data-loading

github.com/draekien開発158 回インストール2 週間前に更新
リポジトリ

tanstack-data-loading について

tanstack-data-loading は、TanStack React Start プロジェクトのデータ読み込み実装とレビューを支援するコーディングスキルです。プロジェクト構成とバージョンを確認し、ローダー、SSR、コンポーネント、ミューテーションのルールを適用します。

主な機能

  1. 01実装、監査、更新の各モード
  2. 02アプリ構成の検出とローカルキャッシュ
  3. 03TanStack 依存バージョンの確認
  4. 04ローダーの並行処理とエラー処理の指針
  5. 05SSR、コンポーネント、ミューテーションのルール

活用シーン

  1. 01ルートローダーのリクエストウォーターフォール監査
  2. 02アプリのデータ読み込み検出記録の更新
  3. 03検出したバックエンドに沿ったサーバー関数の実装

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/draekien/skills --skill tanstack-data-loading

インストール後、Claude Code で/tanstack-data-loadingを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
audit.md
backend-database.md
backend-service.md
versions.md
scripts/
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使用方法のエラー標準エラー出力を確認し、呼び出しを修正して再実行します。

検出。 各フィールドの値はコードから確認します。

フィールド値確認方法
backenddatabase、service、bothcreateServerFn のハンドラーを読みます。database:データベースクライアントに直接クエリを実行します。service:HTTP 経由で別のサービスを呼び出します。both:両方を行うものがあります。
pendingUiroute-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.0fetchQuery、prefetchQuery、ensureQueryData およびそれらの無限クエリ版を使用します。これらはそのバージョンでは非推奨ではありません
staleTime: "static"@tanstack/react-query 5.79.0ensureQueryData(options) を使用します。ローダー内で同じ動作をします
import { noop } from "@tanstack/react-query"@tanstack/react-query 5.76.0.catch(() => {}) を使用します
ミューテーションの scope: { id }@tanstack/react-query 5.32.0isPending の間はトリガーを無効にします
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 のセクション内にエラーが表示されるのではなく、ルートのエラーコンポーネントが表示されます。

コメント