はじめに
Reactでデータを取得するとき、悩みの種になるのが「どのコンポーネントが何のデータを必要としているか」の管理です。素朴にfetchやuseEffectを積み重ねると、画面が育つにつれてウォーターフォール状のリクエストが増え、逆に一箇所でまとめて取得すると今度はオーバーフェッチや、使っていないデータへの依存が発生します。
Relayはこの問題をGraphQLのフラグメント合成と正規化キャッシュで解決してきましたが、GraphQLサーバーの構築コストは決して低くありません。一方でtRPCは型安全なRPCとして手軽に使える反面、Relayのようなビュー合成やキャッシュ管理の仕組みは持っていません。
この両者の「いいとこ取り」を狙って登場したのが、Jestやmetro-bundlerの開発で知られるChristoph Pojer氏が手がける新しいデータクライアント「Fate」です。
Fateとは
Fate(react-fate)は、RelayとGraphQLの設計思想をtRPCのような型安全なRPCの世界に持ち込むデータクライアントです。GraphQLサーバーを用意しなくても、コンポーネントごとに必要なデータを「View」として宣言し、画面全体では単一のリクエストにまとめて取得する、というRelayに近い開発体験を実現します。
なお本記事執筆時点(2026年8月、react-fate 1.3.3)ではREADME上で「現在アルファ版であり、本番環境向けではない」と明記されています。採用を検討する場合は、この点をふまえて評価してください。
主な特徴
- View Composition - コンポーネントごとに必要なフィールドを
view()で宣言し、画面単位で1つのリクエストに合成する - 正規化キャッシュ - データを
__typename:id(例:Post:123)単位でキャッシュし、同じデータへの重複リクエストや重複保持を防ぐ - Data Masking - コンポーネントが宣言したフィールド以外にはアクセスできないようにし、暗黙の依存を防ぐ
- Async React対応 - Suspenseや
useActionState、useといった最新のReact機能と組み合わせて使える - Live Views - Server-Sent Eventsを使い、データ更新をリアルタイムにコンポーネントへ反映できる
- 型安全性 - TypeScriptの型推論に加え、明示的なデータ選択によりAIツールによるコード生成とも相性がよい
インストール
Reactで使う場合(React 19.2以上が必須です)。
npm add react-fate
# または
pnpm add react-fate
# または
yarn add react-fate
Vueで使う場合はvue-fateを、サーバー側の実装には@nkzw/fateを追加します。
npm add vue-fate # Vue向け
npm add @nkzw/fate # サーバーサイド実装
基本的な使い方
Fateの基本は、コンポーネントが必要とするフィールドをview()で宣言することから始まります。view<Post>()のようにデータ型を指定し、取得したいフィールドをtrueで列挙します。
import { view } from 'react-fate';
type Post = {
content: string;
id: string;
title: string;
};
export const PostView = view<Post>()({
content: true,
id: true,
title: true,
});
Viewは合成可能です。関連データを含めたい場合は、別のViewをそのままフィールドとして埋め込みます。
export const UserView = view<User>()({
id: true,
name: true,
profilePicture: true,
});
export const PostView = view<Post>()({
author: UserView, // Userのview結果をauthorとして合成
content: true,
id: true,
title: true,
});
画面のルートではuseRequestでデータをまとめて要求し、コンポーネント側ではuseViewで自分が宣言したViewだけを取り出します。データはグローバルな正規化キャッシュを通じて共有されるため、同じPostを複数のコンポーネントが参照しても二重にリクエストされることはありません。
import { useRequest, useView, ViewRef } from 'react-fate';
export function App() {
const { posts } = useRequest({ posts: { list: PostView } });
return posts.map((post) => <PostCard key={post.id} post={post} />);
}
function PostCard({ post: postRef }: { post: ViewRef<'Post'> }) {
const post = useView(PostView, postRef);
return (
<article>
<h2>{post.title}</h2>
<p>{post.content}</p>
</article>
);
}
useRequestはIDを指定した単一取得や複数ID取得、argsによるフィルタリング・ソートにも対応しています。
// IDで1件取得
const { post } = useRequest({ post: { id: '12', view: PostView } });
// 複数IDをまとめて取得
const { posts } = useRequest({ posts: { ids: ['6', '7'], view: PostView } });
// 引数付きリクエスト(先頭10件など)
const { posts } = useRequest({
posts: { args: { first: 10 }, list: PostView },
});
なお、FateはuseRequestが実際にサーバー(@nkzw/fateで構築するtRPC的なエンドポイント)へリクエストを送ることで初めて動作する、フルスタックのデータクライアントです。GraphQLクライアントと同様にバックエンドとセットで機能する設計のため、本記事ではブラウザ単体で完結する実行可能サンプルは用意していません。実際の挙動を試す場合は、公式リポジトリのexampleを手元で動かすことをおすすめします。
実践的なユースケース
ページネーション対応のリスト(useListView)
コメント一覧のような「もっと見る」で追加読み込みするUIは、Connection形式のViewとuseListViewで実装します。args: { first: 10 }のように初期取得件数を指定し、loadNextで次ページを読み込みます。
const CommentView = view<Comment>()({ content: true, id: true });
const CommentConnectionView = {
args: { first: 10 },
items: { node: CommentView },
};
function PostCard({ post: postRef }: { post: ViewRef<'Post'> }) {
const post = useView(PostView, postRef);
const [comments, loadNext] = useListView(CommentConnectionView, post.comments);
return (
<div>
{comments.map(({ node }) => (
<CommentCard comment={node} key={node.id} />
))}
{loadNext ? <button onClick={loadNext}>Load more</button> : null}
</div>
);
}
ミューテーションと楽観的更新
「いいね」ボタンのように即座にUIを反映したい操作は、React ActionsのuseActionStateとoptimisticオプションを組み合わせます。サーバーの応答を待たずにlikesを先に加算しておき、失敗時はFateが自動でロールバックします。
const LikeButton = ({ post }: { post: { id: string; likes: number } }) => {
const fate = useFateClient();
const [result, like] = useActionState(fate.actions.post.like, null);
return (
<button
onClick={() =>
like({
input: { id: post.id },
optimistic: { likes: post.likes + 1 }, // 先にUIを更新
})
}
>
{result?.error ? 'Oops!' : `Like (${post.likes})`}
</button>
);
};
Live Views(SSE)によるリアルタイム更新
いいね数やステータスのように、他ユーザーの操作でも変化する値はuseViewの代わりにuseLiveViewを使うと、Server-Sent Events経由でサーバー側の変更を自動的に画面へ反映できます。
import { useLiveView, ViewRef } from 'react-fate';
export const PostCard = ({ post: postRef }: { post: ViewRef<'Post'> }) => {
const post = useLiveView(PostView, postRef);
return (
<article>
<h2>{post.title}</h2>
<p>{post.likes} likes</p> {/* 他ユーザーの操作でも自動更新される */}
</article>
);
};
サーバー側ではcreateLiveEventBusで作ったイベントバスをcreateFateServerに渡し、データ変更のタイミングでlive.update('Post', id, { changed: ['likes'] })を呼び出すことで、購読中のクライアントに変更を配信します。
まとめ
Fateは、RelayとGraphQLが培ってきた「View合成による宣言的なデータ取得」と「正規化キャッシュによる効率的な更新」を、tRPCのような型安全なRPCの世界に持ち込むデータクライアントです。view()によるフィールド宣言、useRequest/useViewによるデータ取得、useListViewによるページネーション、useLiveViewによるリアルタイム更新まで、Relay経験者にはなじみ深い機能が一通り揃っています。
ただし現時点ではアルファ版であり、APIも変化していく段階です。tRPCベースのプロジェクトでRelayライクなデータ管理を検討しているなら、まずは公式リポジトリのexampleを動かしてAPIの雰囲気をつかんでみるのがよいでしょう。
