はじめに
GraphQLクライアントを選ぶとき、「Apollo Clientでいいか」で思考を止めていませんか。
小〜中規模のアプリならそれで十分です。ですが、コンポーネント数が数百、数千を超えてくると、「このコンポーネントが実際どのデータに依存しているか」が曖昧になり、無駄なフィールドの取得や、修正時の見落としによるバグが増えていきます。
Relayは、まさにこの問題のために作られたGraphQLクライアントです。Facebook本体で数万のコンポーネントを支えてきた実績があり、コンポーネント単位でデータ依存を宣言し、コンパイラがそれを集約・最適化するという、他のGraphQLクライアントとは一線を画すアーキテクチャを採用しています。
この記事では、Relayの特徴からインストール、実際のコード例までを解説します。
Relayとは
Relayは、Facebook(Meta)が開発・保守する、Reactのためのスケーラブルなデータドリブンフレームワークです。単なるGraphQLクライアントというより、「データ取得のためのコンパイラ付きフレームワーク」と呼ぶ方が実態に近いです。
npmパッケージとしては react-relay(Reactバインディング)、relay-runtime(コア機能)、relay-compiler(ビルド時最適化ツール)の組み合わせで構成されており、記事執筆時点の最新バージョンはいずれも 21.0.1 です。
主な特徴
- コンパイラによるクエリ最適化 - 各コンポーネントが宣言した
graphqlクエリをビルド時に集約し、重複フィールドを排除した単一の効率的なリクエストへ最適化します - フラグメントによるデータのコロケーション - コンポーネントは自分が使うフィールドだけを
useFragmentで宣言でき、親コンポーネントが子の内部データ要件を意識する必要がありません - 型安全性 -
relay-compilerがGraphQLスキーマから TypeScript / Flow の型定義を自動生成し、存在しないフィールドを参照するとビルドエラーで検出できます - 正規化されたキャッシュ - 取得したデータをID単位で正規化してストアに保持するため、同じエンティティを参照する複数コンポーネントが自動的に最新状態に同期されます
インストール
Relayを使うには、ランタイムに加えてrelay-compilerと、ビルドツール向けのプラグインが必要です。
# npm
npm install react-relay relay-runtime
npm install --save-dev relay-compiler babel-plugin-relay graphql
# yarn
yarn add react-relay relay-runtime
yarn add -D relay-compiler babel-plugin-relay graphql
Babelを使わずViteなどでビルドしている場合は、vite-plugin-relayのような各ビルドツール向けプラグインを別途導入します。
基本的な使い方
Relayは他のGraphQLクライアントと違い、ビルド時にrelay-compilerを実行してクエリをコンパイルすることが前提のアーキテクチャです。この性質上、ブラウザ単体で完結する実行環境(LiveCodesのようなオンラインプレイグラウンド)では動かせません。あらかじめご了承ください。以下は実際のプロジェクト構成を想定したコード例です。
まず、アプリのエントリーポイントでRelayEnvironmentを用意し、GraphQLサーバーへの通信方法を定義します。
import { Environment, Network, Store, RecordSource } from 'relay-runtime';
const fetchFn = async (params: { text: string | null }, variables: object) => {
const response = await fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: params.text, variables }),
});
return response.json();
};
export const environment = new Environment({
network: Network.create(fetchFn),
store: new Store(new RecordSource()),
});
次にRelayEnvironmentProviderでアプリ全体をラップし、コンポーネント内でuseLazyLoadQueryを使ってデータを取得します。graphqlタグで書かれたクエリは、relay-compilerが事前に静的解析してビルド時に最適化済みの形へ変換します。
import { graphql, useLazyLoadQuery } from 'react-relay';
const UserQuery = graphql`
query UserProfileQuery($userId: ID!) {
user(id: $userId) {
name
email
}
}
`;
function UserProfile({ userId }: { userId: string }) {
const data = useLazyLoadQuery(UserQuery, { userId });
return <p>{data.user.name} / {data.user.email}</p>;
}
この時点でRelayらしさはまだ薄いですが、次の「実践的なユースケース」で紹介するフラグメントを使うと、コンポーネント単位でのデータ依存管理という真価が見えてきます。
実践的なユースケース
フラグメントによるデータのコロケーション
子コンポーネントが自分の描画に必要なフィールドだけをgraphqlフラグメントとして宣言し、useFragmentで受け取るパターンです。親コンポーネントは子が内部で何のフィールドを使っているかを一切知る必要がなく、UIとデータ要件が同じファイル内で完結します。
import { graphql, useFragment } from 'react-relay';
const AvatarFragment = graphql`
fragment Avatar_user on User {
name
avatarUrl
}
`;
function Avatar({ userRef }: { userRef: any }) {
const user = useFragment(AvatarFragment, userRef);
return <img src={user.avatarUrl} alt={user.name} />;
}
親クエリ側は...Avatar_userとフラグメントをスプレッドするだけでよく、Avatarコンポーネントがフィールドを追加してもクエリの書き換えは不要です。大規模なコンポーネントツリーほど、この分離のメリットが効いてきます。
useMutationによるデータ更新
サーバー側のデータを変更する処理にはuseMutationを使います。Relayはミューテーション実行後、レスポンスに含まれる更新済みフィールドを使って、正規化ストア内の該当レコードを自動的に更新します。
import { graphql, useMutation } from 'react-relay';
const LikePostMutation = graphql`
mutation LikePostMutation($postId: ID!) {
likePost(postId: $postId) {
post {
id
likeCount
}
}
}
`;
function LikeButton({ postId }: { postId: string }) {
const [commit, isInFlight] = useMutation(LikePostMutation);
return (
<button
disabled={isInFlight}
onClick={() => commit({ variables: { postId } })}
>
いいね
</button>
);
}
likePostが返すpost.likeCountは、同じ投稿を参照している他のコンポーネント(例えば投稿一覧のカード)にも自動で反映されます。手動でキャッシュを無効化するコードを書く必要がない点が、正規化ストアを持つRelayの強みです。
usePaginationFragmentによる無限スクロール
コメント一覧や投稿フィードのような「もっと見る」型のUIには、usePaginationFragmentが使えます。ページネーションのロジック(カーソル管理、次ページ取得)をフック側に任せられます。
import { graphql, usePaginationFragment } from 'react-relay';
const CommentListFragment = graphql`
fragment CommentList_post on Post
@refetchable(queryName: "CommentListPaginationQuery") {
comments(first: $count, after: $cursor)
@connection(key: "CommentList_comments") {
edges {
node {
id
body
}
}
}
}
`;
function CommentList({ postRef }: { postRef: any }) {
const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment(
CommentListFragment,
postRef,
);
return (
<div>
{data.comments.edges.map(({ node }) => (
<p key={node.id}>{node.body}</p>
))}
{hasNext && (
<button disabled={isLoadingNext} onClick={() => loadNext(10)}>
もっと見る
</button>
)}
</div>
);
}
@connectionディレクティブを付けることで、新しく取得したページのデータが既存のリストへ自動的に追記されます。ページ番号やオフセットを自前で管理する必要がないのは、地味ながらもかなり楽になるポイントです。
まとめ
Relayは、GraphQLスキーマの恩恵をコンパイル時の最適化と型安全性に振り切って引き出すフレームワークです。学習コストは他のGraphQLクライアントよりやや高く、relay-compilerのビルド設定という前提知識も必要になりますが、その分「コンポーネントが増えても破綻しないデータ取得層」という見返りがあります。
小規模なプロジェクトでは過剰装備に感じるかもしれませんが、コンポーネント数が数百を超えるような大規模なReactアプリケーションを設計しているなら、Relayを候補に入れる価値は十分にあります。まずは公式ドキュメントのチュートリアルから、graphqlタグとフラグメントの書き方に触れてみてください。