GraphQL

GraphQLのFragment(フラグメント)とは?使い方・インラインフラグメント・Colocation・@deferを実例で解説

GraphQLのFragment(フラグメント)とは?使い方・インラインフラグメント・Colocation・@deferを実例で解説

GraphQLのFragment(フラグメント)は、型を指定したフィールドの組に名前を付け、複数のクエリから...名前で差し込めるようにする仕組みです。この記事では、仕様の最新版(September 2025)の該当節とgraphql-js 17.0.2での実測をもとに、書き方、インラインフラグメント(... on)の使いどころ、検証で弾かれるパターン、コンポーネント単位でフラグメントを持たせるColocation、フラグメント単位で後から返させる@deferまでを順に説明します。

まとめ:GraphQL Fragmentの要点

  • 定義:fragment 名前 on 型 { フィールド }で定義し、...名前で展開します。展開先の型とonの型が重ならないと検証エラーです。
  • インラインフラグメント:union・interfaceの返り値から型ごとにフィールドを取るときは... on 型を使います。型条件を省けば@include・@skipでフィールドをまとめて出し分けられます。
  • 検証ルール:未使用・名前重複・循環・存在しない型・スカラー型への定義は、どれも仕様5.5節でエラーと定められています。
  • 性能:フラグメントを使ってもサーバーが返すJSONは同じです。速くなるのは記述と保守で、通信量ではありません。
  • Colocation:表示コンポーネントの隣にフラグメントを置き、親のクエリで1本に合成する設計です。RelayやApollo Client 3.12以降のデータマスキングと組み合わせると、親が子のフィールドへ触れなくなります。
  • @defer:付けられるのはフラグメントの展開とインラインフラグメントだけで、フィールドには付けられません。仕様には未収録(RFC 2のDraft)で、Apollo Client 4系ではincrementalHandlerを設定しないと使えません。
  • フラグメント引数:仕様には未収録(RFC 2のDraft)です。graphql-js 17では実験オプションでのみ解釈され、実務で使える仕組みはRelayの@argumentDefinitionsです。

GraphQL Fragmentの定義と書き方

フラグメントは仕様の2.9節「Fragments」で定義されている、選択セット(フィールドの組)の再利用単位です。同じユーザー情報を一覧・詳細・コメント欄の3つのクエリで取る場面を考えると、フィールドを足すたびに3か所を直す手間がなくなります。

名前付きフラグメントの構文とスプレッド

fragmentキーワード、フラグメント名、onに続く型条件、選択セットの順に書きます。展開するときは名前の前にドットを3つ付けます。

fragment UserBasic on User {
  id
  name
}

query GetUser {
  user(id: "1") {
    ...UserBasic
    email
  }
}

フラグメントの外にフィールドを並べてもかまいません。上の例ではUserBasicのid・nameにemailを足しています。フラグメントの中から別のフラグメントを展開することもでき、入れ子の深さに仕様上の上限はありません。ただし循環すると検証エラーになります(後述)。

フラグメント内での変数参照とレスポンスの形

フラグメントの中では、それを使う操作(query・mutation)で宣言された変数をそのまま参照できます。逆にいえば、フラグメントを使う操作のすべてでその変数を宣言しておく必要があります。

レスポンスにフラグメント名は残りません。graphql-js 17.0.2で上のクエリと、フラグメントを使わずid name emailと直書きしたクエリを実行したところ、返るJSONは次の1行で、両者は1バイトも違いませんでした。

{"data":{"user":{"id":"1","name":"Sato","email":"[email protected]"}}}

インラインフラグメント(… on)とunion・interfaceの取り分け

インラインフラグメントは名前を持たないフラグメントで、仕様2.9.2節で定義されています。その場で... on 型 { }と書き、主に返り値の型が実行時まで決まらないフィールドで使います。

union・interfaceで型ごとにフィールドを取る書き方

検索結果がUserかPostのどちらかになるunion ResultItem = User | Postでは、union自体はフィールドを持ちません。search { id }と書くと、graphql-js 17.0.2は次のエラーを返しました。

Cannot query field "id" on type "ResultItem". Did you mean to use an inline fragment on "Node", "Post", or "User"?

型ごとに... onで分け、クライアント側で判定できるよう__typenameを一緒に取ります。

query FindItems {
  search(q: "graphql") {
    __typename
    ... on User { name }
    ... on Post { title }
  }
}
{"data":{"search":[{"__typename":"User","name":"Sato"},{"__typename":"Post","title":"Hello"}]}}

interfaceの場合は共通フィールド(例:Nodeのid)を外側に書き、実装型ごとのフィールドだけを... onで取ります。型の切り分けは名前付きフラグメントでもできるので、同じ型別の取り方を複数のクエリで使うならfragment UserHit on Userのように名前を付けて共有します。

型条件なしのインラインフラグメントと@include・@skip

仕様2.9.2節には「TypeConditionを省略したインラインフラグメントは、外側と同じ型とみなす」とあります。これは型の切り分けではなく、複数フィールドにまとめてディレクティブを掛けるための書き方です。

query GetUser($full: Boolean!) {
  user(id: "1") {
    id
    ... @include(if: $full) {
      name
      email
    }
  }
}

この例では、nameとemailにそれぞれ@includeを付ける代わりに、1回でまとめて指定できます。...記法は、型条件があれば型の切り分け、なければディレクティブのまとめ役、と覚えておけば迷いません。

Apollo ClientのpossibleTypes設定

union・interfaceへのフラグメントは、サーバー側では問題なく動いても、Apollo Clientのキャッシュ側で設定が要ります。公式ドキュメント(Fragments)は、InMemoryCacheの初期化時にpossibleTypesを渡す必要があると明記しています。これはunion・interface名から、それに属する具体型の一覧への対応表です。

import { InMemoryCache } from "@apollo/client";

const cache = new InMemoryCache({
  possibleTypes: {
    ResultItem: ["User", "Post"],
    Node: ["User", "Post"],
  },
});

手書きするとスキーマに型を足したときに更新を忘れるので、公式はイントロスペクションの結果からJSONを生成するスクリプトか、GraphQL Code Generatorの@graphql-codegen/fragment-matcherプラグイン(2026年10月時点で7.1.0)での生成を案内しています。生成したJSONをpossibleTypesに読み込ませ、最新のスキーマからJSONを再生成する処理をビルド前に実行すれば、スキーマの変更を反映できます。旧版の記事やApollo Client 2系の情報に出てくるIntrospectionFragmentMatcherは2系までの仕組みで、3系以降のコードでは使いません。Apollo Client全体の構成と4系の変更点はApollo Clientの使い方とv4の変更点で扱っています。

フラグメントが検証で弾かれるパターンとエラーメッセージ

フラグメントの誤りは実行前の検証段階で止まります。仕様5.5節の検証ルールそれぞれに違反する文書を作り、graphql-js 17.0.2(Node.js 26.5.0)のvalidate()に掛けた結果が次の表です。エラーメッセージで検索したときに、どのルールに当たっているかを引けるようにしています。

仕様の節 ルール 違反の例
5.5.1.1 名前の一意性 同名のfragmentを2つ定義
5.5.1.2 型の存在 on Usr(綴り誤り)
5.5.1.3 複合型のみ on String
5.5.1.4 未使用の禁止 定義だけで展開しない
5.5.2.1 展開先の定義 存在しない…Nopeを展開
5.5.2.2 循環の禁止 AがBを、BがAを展開
5.5.2.3 展開の可能性 User上でon Postを展開

それぞれのエラーメッセージは次のとおりです(上の表と同じ順)。

There can be only one fragment named "A".
Unknown type "Usr". Did you mean "User"?
Fragment "S" cannot condition on non composite type "String".
Fragment "UserBasic" is never used.
Unknown fragment "Nope".
Cannot spread fragment "A" within itself via "B".
Fragment "P" cannot be spread here as objects of type "User" can never be of type "Post".

クエリからフラグメントの展開を消したのに定義を残すと、そのクエリ文書全体が「未使用の禁止」違反でエラーになります。Apollo Clientでgqlのテンプレートリテラルにフラグメント定義を埋め込んでいる場合、使わなくなった定義がそのまま本番のエラーになるので、GraphQL Code GeneratorやESLintのGraphQLプラグインのように、ビルド時にスキーマ検証を通す仕組みを入れておくべきです。

同じフィールドを別の引数で取るときの衝突とエイリアス

2つのフラグメントが同じフィールドを違う引数で取ると、仕様5.3.2節「Field Selection Merging」(以前の版の「Overlapping Fields Can Be Merged」)に反して検証で止まります。画像サイズを変えて同じavatarを取る例です。

fragment SmallAvatar on User { avatar(size: 64) }
fragment LargeAvatar on User { avatar(size: 128) }

query { user(id: "1") { ...SmallAvatar ...LargeAvatar } }
Fields "avatar" conflict because they have differing arguments. Use different aliases on the fields to fetch both if this was intentional.

レスポンスのキーが両方ともavatarになり、どちらの値を入れるか決められないためです。メッセージどおり、small: avatar(size: 64)とlarge: avatar(size: 128)のようにエイリアスで名前を分ければ通ります。フラグメントを別々のコンポーネントで書いていると、合成した親クエリで初めてこの衝突が見つかるので、コンポーネント側のフラグメントには用途を表すエイリアスを付けておくと安全です。

Fragment Colocation:コンポーネントごとにデータ依存を宣言する設計

Fragment Colocation(コロケーション)は、画面の部品ごとに「自分が表示に使うフィールド」をフラグメントとして部品の隣に置く設計です。ページのクエリにすべてのフィールドを直書きすると、子の部品がemailを表示しなくなっても、親のクエリからemailを消してよいのかを判断するには全部品を読む必要があります。Colocationでは部品とフラグメントが1対1なので、部品を消せばフラグメントも消せます。

親クエリへの合成と1リクエストへのまとめ方

部品ごとにクエリを発行すると、ページ表示に複数回の往復がかかります。Colocationでは部品が持つのはフラグメントだけで、ページの親が1本のクエリに合成して1回で取得します。

// UserCard.tsx
import { gql } from "@apollo/client";

export const USER_CARD = gql`
  fragment UserCard on User { id name avatar(size: 64) }
`;

// ProfilePage.tsx
import { gql } from "@apollo/client";
import { USER_CARD } from "./UserCard";

const PROFILE = gql`
  query Profile($id: ID!) {
    user(id: $id) { ...UserCard email }
  }
  ${USER_CARD}
`;

${USER_CARD}でフラグメント定義のDocumentNodeを補間しているのは、GraphQLの文書は自己完結している必要があり、展開する...UserCardの定義が同じ文書に無いと「Unknown fragment」になるためです。埋め込みを書き忘れやすい場合、Apollo ClientではcreateFragmentRegistryでフラグメントを登録すると、文字列の埋め込みなしに名前で参照できます。

データマスキングによる親子間のフィールド参照制限

Colocationだけでは、親コンポーネントが子のフラグメントで取ったフィールドを勝手に読めてしまいます。子がavatarを外したら、それを読んでいた親が壊れる、という依存が残るわけです。データマスキングは、親が受け取るデータから子のフラグメントのフィールドを隠し、子は自分のフラグメント経由でしか読めないようにする仕組みです。

ライブラリ マスキング 子が読む手段
Relay(react-relay 21.0.1) 既定で有効 useFragment
Apollo Client(3.12以降・最新4.3.5) dataMasking: trueで有効 useFragment
GraphQL Code Generator client preset 型の上で既定有効 useFragment関数

Apollo ClientのdataMaskingとclient presetのFragment Maskingは同時には使いません。Apollo Clientの公式ドキュメントはclient presetのFragment Maskingを非互換の機能としており、Apollo側のマスキングを使う設定例ではclient presetにfragmentMasking: falseを指定し、子コンポーネントはApolloのuseFragmentで読む形にしています。

Apollo ClientのuseFragmentは3.7.0で実験版として入り、3.8.0で正式版になりました。キャッシュから読むだけでネットワークリクエストは発行しません。4.1.0(2026年1月)からはfromに配列を渡せるようになり、一覧の各要素を1回の呼び出しでまとめて監視できます。データマスキングは3.12.0で追加され、既存アプリ向けには、全スプレッドに@unmaskを付ける、dataMaskingを有効にする、マスク済みの型を生成する、子コンポーネントをuseFragmentへ置き換える、という4段階の移行手順が公式ドキュメントに載っています。client presetの型マスキングの設定と注意点はGraphQL Code Generatorのclient presetとv7の変更点で詳しく扱っています。

Colocationの導入を見送る条件と過分割の失敗例

画面が数枚で、1つのクエリを1つのコンポーネントが表示し切る構成なら、Colocationは入れない方が読みやすくなります。部品ごとのフラグメントとマスキングは、複数の場所で使う部品や、親子間の暗黙のデータ依存が多いアプリで導入効果を判断しやすい設計です。小さな管理画面で全部品にフラグメントを切ると、配置によっては、1フィールドの追加でも表示部品・フラグメント定義・親クエリを行き来する手間が増えます。

もう1つの失敗例は、フラグメントを細かく割りすぎて、同じレスポンスキーに対し、異なる引数で同じフィールドを取るフラグメントが増えることです。前章のSmallAvatarとLargeAvatarのように、別々の部品が同じavatarへ異なるsizeを指定すると、合成先の親クエリでFields "avatar" conflictの検証エラーが出ます。部品を独立させたはずが、合成のたびに親クエリで調整が必要になります。分割の単位は「画面上で独立して置き換わる部品」までに留めるのが目安です。

フラグメントと性能・キャッシュの関係

フラグメントへの書き換えだけで高速化するとは限りません。今回測定したのはレスポンスの一致とクエリ文書の長さで、実行時間は測定していません。前述のとおり、レスポンスはフラグメントの有無で同一でした。保存した検証スクリプト内のクエリ文字列は、フラグメントを使わない版が42バイト、1回使う版が86バイトでした。これは掲載コードの改行・空白を含めた長さではありません。文書が短くなるのは、同じフラグメントを何度も展開したときだけです。

性能に効くのは、フラグメントそのものではなく次の2点です。

  • 取るフィールドを減らす:Colocationで各部品が本当に使うフィールドだけを宣言すると、使われないフィールドが親クエリに残りにくくなります。効くのは「不要フィールドの削除」の方です。
  • キャッシュの同一性:Apollo Clientの正規化キャッシュはオブジェクトを__typenameと識別用フィールドで識別します。既定ではidまたは_idを使い、型ごとのkeyFieldsで変更できます。useFragmentのfromで渡すオブジェクトについては、子のフラグメントではなく親のクエリまたはフラグメントでこの識別用フィールドを選択しておく必要があります。欠けているとキャッシュ上で特定できず、返るdataは不完全になり、コンソールに警告が出ます。

サーバーの負荷には、取得フィールドの深さや数に加え、フラグメントを繰り返し展開する処理のコストも関係します。Apollo Routerのリクエスト制限では、深さ(max_depth)やフィールド数(max_height)をフラグメント内のフィールドも含めて数えるうえ、フラグメントの展開を再帰的にたどる際の選択数にもmax_recursive_selections(既定1,000万)で上限を設けています。自前でクエリの深さや複雑度を制限する場合も、フラグメントを展開した後の形に対して掛けます。スキーマ側の設計判断はGraphQLスキーマ設計のベストプラクティスにまとめています。フラグメントの分け方を含めたAPIの設計から構築までを外部に任せたい場合は、API開発・システム連携の支援で相談を受け付けています。

@deferによるフラグメント単位の遅延配信

@deferは、時間のかかるフィールドの到着を待たずに先にレスポンスを返させ、残りを後から送らせるディレクティブです。付けられる場所はフラグメントの展開(...名前)とインラインフラグメントだけで、「どこまでを後回しにするか」をフラグメントで区切る仕組みになっています。仕様の最新版には入っておらず、提案はgraphql-specのPR #1110(Incremental Delivery)で、2026年10月時点のステージはRFC 2(Draft)です。

graphql-js 17.0.2での書き方と返り方

graphql-js 17.0.2は@deferの定義(GraphQLDeferDirective)を持っていますが、標準のディレクティブ一覧(specifiedDirectives)には含めていません。スキーマに宣言しないまま使うとUnknown directive "@defer".で検証に落ちました。SDLで宣言し、実行にはexperimentalExecuteIncrementally()を使います。

directive @defer(if: Boolean! = true, label: String) on FRAGMENT_SPREAD | INLINE_FRAGMENT

query Profile {
  user(id: "1") {
    id
    name
    ...Reviews @defer(label: "rv")
  }
}

fragment Reviews on User { reviews }

1行目はスキーマ側のSDLに書く宣言で、2行目以降がクライアントから送るクエリです。reviewsの解決に50ミリ秒かかるリゾルバで実行すると、最初の応答にはidとnameだけが入り、reviewsは2回目の応答で届きました。

{"data":{"user":{"id":"1","name":"A"}},"pending":[{"id":"0","path":["user"],"label":"rv"}],"hasNext":true}
{"hasNext":false,"incremental":[{"id":"0","data":{"reviews":["good","bad"]}}],"completed":[{"id":"0"}]}

インラインフラグメント(... @defer(label: "rv") { reviews })でも同じ2回の応答になりました。弾かれたのは次の2つです。

Directive "@defer" may not be used on FIELD.
Value for arguments "defer(label:)" and "stream(label:)" must be unique across all Defer/Stream directive usages.

1つ目はreviews @deferのようにフィールドへ直接付けた場合、2つ目は同じlabelを2か所で使った場合です。後回しにしたいフィールドが1つでも、型条件なしのインラインフラグメントで包めば付けられます。

Apollo Clientで@deferの応答を受ける設定

Apollo Client 4.0.0では、@deferの応答の解釈をincrementalHandlerオプションで選ぶ方式になりました。既定のNotImplementedHandlerは、@deferを含む操作を送ると例外を投げます。上のpending・completedを含む新しい形式を受けるには4.1.0で追加されたGraphQL17Alpha9Handlerを、Apollo Routerが返す従来の形式にはDefer20220824Handlerを指定します。サーバーと違う形式のハンドラを選ぶと正しく組み立てられないので、まずサーバーがどちらの形式を返すかを確かめます。

import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client";
import { GraphQL17Alpha9Handler } from "@apollo/client/incremental";

const client = new ApolloClient({
  cache: new InMemoryCache(),
  link: new HttpLink({ uri: "/graphql" }),
  incrementalHandler: new GraphQL17Alpha9Handler(),
});

部品側では、3.13.0で追加されたuseSuspenseFragmentを使うと、後回しにしたフラグメントのデータがそろうまでその部品だけをSuspenseで待たせられます。公式ドキュメントは、fromでキャッシュ上の対象を特定するための識別用フィールドは@deferの外に置くよう注意しています。Apollo Routerを使ったサブグラフ構成で@deferを通す場合は、GraphQLフェデレーションのサブグラフ合成とルーターの実装も合わせて確認してください。

フラグメント引数(Fragment Arguments)の現状

フラグメントの中で使う値を展開側から渡したい、という要望はGraphQLで長く議論されています。2026年10月時点の状況は次のとおりです。

  • 仕様:最新版のSeptember 2025にも、2026年9月28日付のWorking Draftにも、フラグメント引数の構文はありません。提案はgraphql-specのPR #1224で、ステージはRFC 2(Draft)、推進役を募る「Needs Champion」のラベルが付いています。
  • graphql-js 17系:2026年6月15日に正式公開された17.0.0から、フラグメント引数を実験機能として実装しています。17.0.2ではparse()にexperimentalFragmentArguments: trueを渡したときだけ解釈します。渡さないとSyntax Error: Expected "on", found "(".で止まります。
  • Relay:@argumentDefinitionsと@argumentsというディレクティブで、仕様の外側で同じことを実現しています。
fragment Avatar($size: Int = 64) on User {
  avatar(size: $size)
}

query Q { user(id: "1") { ...Avatar(size: 128) } }

graphql-js 17.0.2で上の文書を実験オプション付きで実行すると、sizeを渡せばimg_128.png、省略すれば既定値のimg_64.pngが返りました。ただし、上の例のようにフラグメント定義を操作より前に書くと、validate()が検証エラーではなく例外(TypeError)で落ちました。操作を先に書けば検証も実行も通ります。仕様に入っていない機能で、実装側もこの程度に未成熟なので、本番のクエリには使わず、必要ならRelayのディレクティブで代替するのが現実的です。

よくある質問

フラグメントとインラインフラグメントの違いは何ですか?

名前を付けて定義し複数か所から...名前で再利用するのがフラグメント、名前を付けずにその場で... on 型と書くのがインラインフラグメントです。インラインフラグメントは主にunion・interfaceの型ごとの取り分けと、@include・@skipをまとめて掛ける用途で使います。

フラグメントは.graphqlファイルに分けて管理できますか?

できます。GraphQL Code Generatorはdocumentsに指定した.graphqlファイルやソース中のgqlからフラグメントを集めて型を生成します。webpackで.graphqlファイルを直接読み込む場合、Apollo Clientの公式ドキュメントはgraphql-tag/loaderの#import文で別ファイルのフラグメントを取り込む方法を案内しています。

gqlタグの中で別のフラグメントを使うにはどうしますか?

gqlで作成したフラグメント定義のDocumentNodeを、${USER_CARD}のようにテンプレートリテラルへ補間します。埋め込まないと同じ文書に定義が無いため「Unknown fragment」の検証エラーになります。Apollo ClientではcreateFragmentRegistryに登録すれば、埋め込みなしで名前参照できます。

フラグメントを使うとレスポンスは軽くなりますか?

軽くなりません。graphql-js 17.0.2の実測では、レスポンスは直書きのクエリと1バイトも変わりませんでした。軽くなるのは取得フィールドを減らしたときです。

フラグメントの中で別のフラグメントを使えますか?

使えます。フラグメントの選択セットに...OtherFragmentを書けば入れ子にできます。ただしAがBを、BがAを展開するような循環は仕様5.5.2.2節で禁止されており、Cannot spread fragment "A" within itself via "B".のエラーになります。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.29 テックブログ ソニー生命の不正アクセス:38.5万件の対象確認と窓口、停止中の手続き
  2. 2026.10.31 テックブログ osinaの情報漏洩:約23万人の対象確認とSNS連携の解除、分析ツールへ渡すデータの絞り方
  3. 2026.10.31 テックブログ 松屋の不正アクセス:237万件の対象確認と窓口、漏れたパスワードの守り方
  4. 2026.11.01 テックブログ RAGTAGの情報漏洩:延べ約120万件の対象項目と会員の備え、侵入から検知まで11日の意味
  5. 2026.05.18 テックブログ マネーフォワードで連携できない・失敗するときの対処法|エラー別の原因と再連携のやり方

RELATED POSTS 関連記事

目次