mcpField
R
relay-best-practices

relay-best-practices

github.com/facebook開発19.0k214 回インストール今日更新
リポジトリ

relay-best-practices について

relay-best-practicesは、Relayを使うReact開発者向けのガイダンススキルです。GraphQLのデータ取得、コンポーネントのフラグメント、コンパイラー、エラー処理を扱い、判断基準とアンチパターンでRelayの設計に沿った実装を支援します。

主な機能

  1. 01GraphQLフラグメントのコンポーネント内配置
  2. 02プリロードクエリのパターン
  3. 03Relayコンパイラーの手順案内
  4. 04フィールドエラー処理の解説
  5. 05よくあるアンチパターンの提示

活用シーン

  1. 01Reactコンポーネントのデータ要件の設計
  2. 02クエリの配置場所の判断
  3. 03Relayのページネーションとミューテーション処理
  4. 04生成型が不足する問題の調査

インストール

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

$npx skills add https://github.com/facebook/relay --skill relay-best-practices

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

詳細

Relay のベストプラクティス

Relay は React 向けの GraphQL クライアントで、データ取得の局所化、コンポーザブルな構成、型安全性を徹底します。中核となる考え方は、各コンポーネントが GraphQL フラグメントを通じて必要なデータを正確に宣言し、取得、キャッシュ、一貫性、更新は Relay が処理するというものです。

このスキルでは、どのパターンを優先すべきかについて、明確な指針を示します。API の詳細なドキュメントについては、以下のドキュメント一覧から該当するページを参照してください。

ドキュメント

Relay には、LLM 向けのドキュメントが node_modules/relay-runtime/llm-docs/ に含まれています(v20.1.1 以降で利用可能です)。それより古いバージョンの場合は、同じファイルを https://raw.githubusercontent.com/facebook/relay/main/website/docs/ から取得してください。

以下のパスはこのディレクトリ(<llm-docs>/)からの相対パスです。Relay コードを書く前に、該当するページを読んでください。主なドキュメントは次のとおりです。

トピックパス
中核となる概念と理念principles-and-architecture/thinking-in-relay.mdx
フラグメントguided-tour/rendering/fragments.mdx
クエリguided-tour/rendering/queries.mdx
ミューテーションguided-tour/updating-data/graphql-mutations.mdx
ページネーションguided-tour/list-data/pagination.mdx
再取得guided-tour/refetching/refetching-queries-with-different-data.mdx
useFragmentapi-reference/hooks/use-fragment.mdx
usePreloadedQueryapi-reference/hooks/use-preloaded-query.mdx
useQueryLoader / loadQueryapi-reference/hooks/load-query.mdx
useMutationapi-reference/hooks/use-mutation.mdx
usePaginationFragmentapi-reference/hooks/use-pagination-fragment.mdx
@throwOnFieldErrorguides/throw-on-field-error-directive.mdx
@catch ディレクティブguides/catch-directive.mdx
セマンティックな null 許容性guides/semantic-nullability.mdx
Relay リゾルバーguides/relay-resolvers/introduction.mdx
テストguides/testing-relay-components.mdx
コンパイラーのセットアップgetting-started/compiler.mdx
コンパイラーの設定getting-started/compiler-config.mdx
lint ルール(ESLint プラグイン)getting-started/lint-rules.mdx

パフォーマンスに特化した指針(クエリの配置、@defer、ページネーション、フェッチポリシー、キャッシュ、フラグメントの粒度)については、関連する relay-performance スキルを参照してください。

中核となる理念

以下の原則が、この先のあらゆる判断の基盤となります。迷ったときは、これらの原則に立ち返ってください。

  • 局所化:各コンポーネントは、レンダリングコードのすぐ近くで GraphQL フラグメントを通じて独自のデータ要件を宣言します。データ要件はコンポーネントとともにあり、切り離されません。
  • データマスキング:コンポーネントがアクセスできるのは、自身のフラグメントで明示的に選択したフィールドのみです。親は子のフラグメントデータを参照できず、その逆も同様です。これにより、コンポーネント間の暗黙的な結合を防ぎます。
  • 合成:フラグメントは親フラグメント、さらにクエリへと合成され、コンポーネントツリーを反映するツリーを形成します。コンパイラーはこれをクエリごとに単一のネットワークリクエストへとフラット化します。
  • 取得しながらレンダリング:必要とするコンポーネントがレンダリングされる前に、データの取得を開始します。これにより、リクエストが逐次的に連なるウォーターフォールを回避できます。
  • 正規化ストア:Relay は ID をキーとするフラットなキャッシュを維持します。ミューテーションが更新されたデータを返すと、そのデータを読み取るすべてのコンポーネントが自動的に再レンダリングされます。変更を手動で伝播する必要はありません。

コンパイラーワークフロー

Relay は事前コンパイラーを使用します。このコンパイラーは、コード内の graphql タグ付きテンプレートリテラルを読み取り、ランタイムアーティファクトと TypeScript/Flow 型を生成します。

設定ファイルの確認

コンパイラーは、以下の場所を順番に確認して設定ファイルを探します。

  • プロジェクトのルートにある relay.config.{json,js,mjs,ts}
  • package.json 内の "relay" キー

設定スキーマの詳細については、<llm-docs>/getting-started/compiler-config.mdx を参照してください。 npx relay-compiler config-json-schema を実行して、設定用の JSON Schema を出力することもできます。

コンパイラーの実行

graphql タグ付きテンプレートリテラルの内容、または Relay Resolver のドックブロックを変更した後は、npx relay-compiler を実行します。プロジェクトによっては、これを package.json のスクリプトとして追加しています(例:yarn relay)。コンパイラーはウォッチモード(--watch)にも対応していますが、プロセスが終了しないため、非対話型のコンテキストでは使用しないでください。

生成されたファイルは、ソースファイルの隣にある __generated__/ ディレクトリに配置されます。これらのファイルは編集しないでください。コンパイラーを実行するたびに上書きされます。生成された型が見つからないという型エラーが表示された場合は、まずコンパイラーを実行してください。型が古くなっているだけの可能性があります。

Lint ルール

Relay の ESLint プラグイン(eslint-plugin-relay)は、開発者体験を支える重要な要素です。特に重要なルールが 2 つあります。

  • relay/unused-fields — フラグメントで選択されているものの、コンポーネントでは読み取られていない GraphQL フィールドを検出します。これにより、フラグメントに未使用のフィールドが時間とともに蓄積し、どのコンポーネントも必要としないデータを取得する「追加専用クエリ」の問題を防ぎます。
  • relay/no-future-added-value — サーバーによって新しい列挙型の値が追加される可能性に対応できるよう、Relay が挿入する "%future added value" 列挙型プレースホルダーを明示的に処理することを防ぎます。

インストールと設定については、<llm-docs>/getting-started/lint-rules.mdx を参照してください。

判断ルール

クエリはルートに配置し、フックには配置しない

クエリはルートのエントリーポイント(URL に対応する最上位コンポーネント)に配置します。フックは葉にあたり、多くのコンポーネントで再利用されます。フック内のクエリは遅いタイミング(フックを含むコンポーネントのレンダー後)で実行され、さらに呼び出し元ごとに重複します。解決策は常に、フラグメントキーをパラメーターとして受け取り、useFragment を使用することです。

usePreloadedQuery と useQueryLoader(または loadQuery)を使用します。イベントハンドラー、ルート遷移、またはアプリの初期化中に、コンポーネントがレンダーされる前にフェッチを開始します。useLazyLoadQuery はレンダーが始まるまでフェッチを開始しないため、ウォーターフォールが発生します。全体のパターンについては、<llm-docs>/guided-tour/rendering/queries.mdx を参照してください。

クエリの配置場所を修正する前に、そのクエリが必要かどうかを確認する
質問YES の場合
親コンポーネントがすでにこの GraphQL 型をフェッチしていますか?クエリを削除し、useFragment を使用します
コンポーネントはデータをフェッチして下位に渡すだけですか?ラッパーコンポーネントを完全に削除します(ローダーのアンチパターン)
クエリはカスタムフック内にありますか?クエリを削除し、フラグメントキーのパラメーターを受け取るようにします
2 つのコンポーネントが同じデータをフェッチしていますか?片方のクエリを削除し、共通の祖先でフェッチします
データはログ記録/分析にしか使われませんか?@defer またはサーバー側のログ記録に移します
データは静的な設定(すべてのユーザーで同じ)ですか?サーバー側で注入し、ラウンドトリップは不要にします
useEffect 内の loadQuery は useLazyLoadQuery よりも悪い

useEffect は描画後に実行されるため、(少なくともレンダー中に開始される)useLazyLoadQuery よりもさらに遅くフェッチが始まります。loadQuery はイベントハンドラー、ルート遷移、またはアプリの初期化中に呼び出し、エフェクト内では呼び出さないでください。

useFragment を使う際に祖先ツリーをたどる

useFragment でクエリを置き換えられるか判断するには、コンポーネントからコンポーネントツリーを上にたどります。ルート境界、機能ゲート、条件付きレンダー、またはユーザーが起こす操作に到達したら止めます。無条件のレンダー、レイアウト/ラッパーコンポーネント、コンテキストプロバイダーはそのままたどります。

必要な GraphQL 型を祖先のいずれかがすでにクエリしているなら、useFragment が答えです。フラグメントキーを複数の props レイヤーにわたって渡すのは問題ありません。これが正しいパターンです。

クエリ変数のデフォルト値に空の値を設定しない

実際の値が取得できないときに、クエリ変数のデフォルト値を ''、0、または null にしないでください。誤ったデータでクエリが実行され、間違った結果やエラーが返されます。代わりに、条件付きレンダー(if (!id) return null)や @include/@skip ディレクティブを使用して、フィールド全体を省略してください。

データフロー:常にフラグメントを使用する

サーバーデータを表示するすべてのコンポーネントは、フラグメントを宣言し、 $key 型のフラグメント参照をプロパティとして受け取る必要があります。親コンポーネントは 子コンポーネントのフラグメントを自身のクエリまたはフラグメントに展開し、その結果を 子コンポーネントに渡します。例については、以下の「フラグメントのコロケーションを 維持する」というアンチパターンを参照してください。

ミューテーション:フラグメントをレスポンスに展開する

フィールドを個別に選択するのではなく、ミューテーションのレスポンスに利用側コンポーネントの フラグメントを展開してください。これにより、自動的に同期されます。以下の アンチパターンの例を参照してください。

エラー処理:@throwOnFieldError と @catch を使用する

フィールドエラーと null 許容性を処理するには、フラグメントまたはクエリに @throwOnFieldError を追加する方法が推奨されます。フィールドエラーが発生すると Relay が JavaScript 例外をスローし、React エラーバウンダリーで捕捉できるようになります。 また、@semanticNonNull フィールドが非 null 型になり、不要な null チェックを なくせます。このパターンは、アプリケーションで React エラーバウンダリーが 設定されていることを前提としています。エラーバウンダリーが堅牢に設定されていない場合は、 慎重に進めてください。

例外をスローするのではなく、エラーをローカルで処理したいフィールドには、 @catch を使用してインラインでエラーを受け取ります。形式は { ok: true, value: T } | { ok: false, errors: [...] } です。

特定のフィールドが非 null であることを宣言するには、@required も使用できますが、 新しいコードでは @throwOnFieldError + @catch のパターンを推奨します。

<llm-docs>/guides/throw-on-field-error-directive.mdx、 <llm-docs>/guides/catch-directive.mdx、 <llm-docs>/guides/semantic-nullability.mdx を参照してください。

ページネーション:3 つのディレクティブを組み合わせる

必ず @argumentDefinitions(カーソル/件数の変数用)、@refetchable (ページネーションクエリの自動生成用)、@connection(ストア管理のための コネクション識別用)を組み合わせて使用してください。手動でページネーションクエリを 作成しないでください。<llm-docs>/guided-tour/list-data/pagination.mdx を 参照してください。

クライアント状態:Relay Resolvers を優先する

複数のコンポーネントがクライアント側のデータを読み取る必要がある場合は、 プロパティのバケツリレーや React コンテキストではなく、Relay Resolvers を使用して GraphQL スキーマにクライアント専用フィールドを定義してください。これにより、 クライアント状態にもサーバーデータと同じ合成可能性とキャッシュの保証が得られます。 リゾルバーで定義されたフィールドのみを読み取るクエリには、useClientQuery を 使用してください。

リゾルバーの定義方法については、<llm-docs>/guides/relay-resolvers/introduction.mdx を参照してください。

重大なアンチパターン

Relay のデータを React の状態にコピーしない

これは最も重要なルールです。useFragment からデータを読み取って useState にコピーしたり、ミューテーションの onCompleted コールバックで その状態を手動で更新したりしないでください。

// WRONG: Copying Relay data into React state
function UserProfile({userKey}) {
  const data = useFragment(UserProfileFragment, userKey);
  const [name, setName] = useState(data.name); // broken

  const [commit] = useMutation(UpdateNameMutation);
  const handleSave = (newName) => {
    commit({
      variables: {name: newName},
      onCompleted: (response) => {
        setName(response.updateName.user.name); // broken
      },
    });
  };
  return <span>{name}</span>;
}

これが誤りである理由:Relay の正規化ストアが唯一の信頼できる情報源です。ミューテーションが 一致する id を持つ更新済みデータを返すと、Relay は useFragment 経由でそのデータを 読み取るすべてのコンポーネントを自動的に更新します。useState にコピーすると、 Relay が更新できない二つ目の信頼できる情報源を作ることになります。アプリ内の別の場所で 別のミューテーション、サブスクリプション、または再取得によってレコードが更新された場合、 コンポーネントには古いデータが表示されます。

// CORRECT: Read directly from the fragment
function UserProfile({userKey}) {
  const data = useFragment(UserProfileFragment, userKey);
  const [commit, isInFlight] = useMutation(UpdateNameMutation);

  const handleSave = (newName) => {
    commit({variables: {name: newName}});
    // No onCompleted needed — Relay updates the store automatically,
    // and useFragment re-renders this component with the new data.
  };
  return <span>{data.name}</span>;
}

同様に、フラグメントキー($key プロパティ)を React の状態に保存しないでください。 Relay は、マウントされているクエリコンポーネントに保持されなくなったデータを ガベージコレクションします。最初にデータを取得したコンポーネントがアンマウントすると、 保存しておいたキーが、ストアに存在しなくなったデータを指す可能性があります。

フラグメントのコロケーションを維持する

親のクエリですべてのデータを取得し、加工していないデータオブジェクトをプロパティとして 子に渡さないでください。これはデータのマスキングを損ない、密結合を生み出します。 子コンポーネントにフィールドを追加するたびに、親のクエリも編集する必要が生じます。 なお、relay/unused-fields リントルールは、親で選択されているものの子でしか使われない フィールドを検出します。これは、フラグメントを切り出す必要があることを示す 良いサインです。

// WRONG: Parent fetches everything, passes raw data
function Parent({queryRef}) {
  const data = usePreloadedQuery(graphql`
    query ParentQuery {
      user {
        name
        email
        avatarUrl
      }
    }
  `, queryRef);
  return <UserCard name={data.user.name} avatarUrl={data.user.avatarUrl} />;
}

// CORRECT: Child declares its own fragment
function Parent({queryRef}) {
  const data = usePreloadedQuery(graphql`
    query ParentQuery {
      user {
        ...UserCard_user
      }
    }
  `, queryRef);
  return <UserCard user={data.user} />;
}
フラグメントをミューテーションレスポンスにスプレッドする

フラグメントとミューテーションレスポンスの両方で、フィールドを個別に選択しないでください。内容にずれが生じます。代わりに、フラグメントをスプレッドしてください。

# WRONG
mutation UpdateUserMutation($input: UpdateUserInput!) {
  updateUser(input: $input) {
    user { id, name, email, avatarUrl }
  }
}

# CORRECT
mutation UpdateUserMutation($input: UpdateUserInput!) {
  updateUser(input: $input) {
    user { ...UserCard_user }
  }
}

正確性

ストアに依存する値には optimisticUpdater を使用する

楽観的な値が現在のストア状態に依存する場合(例:いいね数を増やす場合)は、optimisticResponse ではなく optimisticUpdater を使用してください。複数の楽観的レスポンスが重なると、誤った結果になることがあります。たとえば、同時に実行された2つの「いいね」ミューテーションがどちらもカウントを5と読み取り、5→6→7ではなく、カウントを6に設定します。そのうち1つがロールバックされると、ストアの状態に矛盾が生じます。

影響範囲の広いミューテーション後にデータを無効化する

ミューテーションの副作用が広範囲に及び、単一のレスポンスペイロードでは捉えきれない場合は、対象を絞って無効化するには invalidateRecord() を、全体を無効化するには invalidateStore() を使用してください。マウント済みコンポーネントで useSubscribeToInvalidationState と組み合わせると、古くなったデータを自動的に再取得できます。

古さを明示的に処理する

Relay はデフォルトで、キャッシュされたデータを無期限に新しいものとして扱います。対処方法は2つあります。

  • 時間ベース:Relay Store に queryCacheExpirationTime を設定すると、一定時間後にデータが自動的に古いものとして扱われます。
  • イベントベース:ミューテーションの副作用がレスポンスペイロードの範囲を超える場合は、invalidateRecord() を呼び出します。

古さを明示的に処理しないと、ユーザーが以前訪れた画面に戻った際に、コンポーネントが任意に古いデータを表示する可能性があります。

型安全性のために単一のアーティファクトディレクトリを使用する

コンパイラー設定で artifactDirectory を設定しないと、フラグメント参照がデフォルトで any になり、型安全性が失われる場合があります。これを設定し、バンドラーのモジュール解決設定も合わせてください。<llm-docs>/getting-started/compiler-config.mdx を参照してください。

ストアの操作には @updatable を使用する

文字列ベースのフィールドアクセスによる直接的なストア操作(例:store.get(id).setValue(newName, 'name'))ではなく、型安全な更新可能クエリ/フラグメントを優先してください。更新可能フラグメントではゲッターとセッターが提供され、タイプミスや型の不一致のリスクを軽減できます。

ミューテーション後の不要な再取得を避ける

ミューテーションレスポンスでコンポーネントのフラグメントをスプレッドすればストアが自動更新される場合は、ミューテーション後に refetch() や fetchQuery() を呼び出さないでください。不要な再取得はネットワークリクエストを無駄にし、UI の更新を遅らせます。

手動での再取得は、ミューテーションの副作用が広範囲に及び、レスポンスペイロードでは捉えきれない場合に限ってください。その場合は、invalidateRecord() を優先してください(上記を参照)。

リアルタイムデータにはサブスクリプションを使用する

常に最新の状態を保つ必要があるデータには、ポーリング(setInterval + fetchQuery)や手動の更新ボタンではなく、GraphQL サブスクリプションを優先してください。サブスクリプションはデータが変更されたときにのみ更新をプッシュし、Relay の正規化ストアと自動的に連携します。

命名規則

Relay では、操作名がその定義元のモジュール(ファイル)と一致することが必須です。名前が一致しない場合、スタイル警告ではなくコンパイラーエラーになります。

要素規則例
フラグメントComponentName_propNameUserCard_user
クエリComponentNameQueryHomePageQuery
ミューテーションComponentNameMutationLikeButtonMutation
生成ファイル__generated__/*.graphql編集しないでください

モジュール名は、すべての拡張子(.react.js、.tsx など)を除いたファイル名です。UserCard.react.js → モジュール名は UserCard です。

新しいファイルに移す際の操作名の変更

コンポーネントを新しいファイルに移す場合は、そのファイル内で定義されている操作名のみを、新しいファイル名に合わせて変更してください。他のモジュールが所有するフラグメントを参照するフラグメントスプレッドの名前は変更しないでください。それらの名前は、定義元のコンポーネントに属します。

操作名を変更すると、生成される型名も変わります(例:UserCard_user$key → ProfileCard_user$key)。そのため、これらの生成型をインポートしているすべての下流コードも更新してください。

__generated__/ ファイルを手動で編集しないでください

次回のコンパイラー実行時に、手動での編集内容はすべて上書きされます。生成された型が見つからないという型エラーが表示された場合は、まずコンパイラーを実行してください。型が存在しないのではなく、古くなっているだけです。

自動フォーマット後にミューテーションの変数キーを確認してください

自動フォーマッターやリンターは変数名を変更し、ミューテーション呼び出しを気付かないうちに壊すことがあります。リンターの自動修正を実行した後は、commit({ variables: { ... } }) の変数キーが、生成された Mutation$variables 型と引き続き一致していることを確認してください(特に data と input の不一致に注意してください)。

コメント