Relay ResolversでクライアントサイドのGraphQLスキーマを拡張する
GraphQLでデータを取得していると、UIの都合でどうしても取得後のデータを加工したいケースが時々発生します。
今回はRelay Resolversを使ってクライアントサイドでGraphQLのスキーマを拡張し、データを任意の形式に加工する方法を紹介します。
前提
- GraphQLクライアントはRelayを使用
- サーバーがRelayのGraphQL Server Specificationを正しく実装している
Relay Resolversとは
Relay Resolverとは、クライアントでのみ使用するデータをスキーマに定義できる仕組みです。
例えば、以下のような独自のフィールドを型安全に定義し、参照することができます。
type User implements Node {
id: ID! # サーバー側で定義されているField
name: String # サーバー側で定義されているField
lastLoginAt: Date # サーバー側で定義されているField
isRecentlyLogin: Boolean # クライアント側で拡張したField
}
Introduction to Relay Resolversには以下のように記述されています
Relay Resolvers are a Relay feature which allow you to augment Relay’s GraphQL graph with values that are known only on the client. This allows you to schematize client state in the same way that you model server state, and to use Relay’s familiar data-fetching APIs to access that state. Client state can include both data from client-side data stores as well as derived data that is computed from other values in the graph.
主にローカルの状態や外部APIのクライアントの状態をクライアント専用のスキーマとして定義する、といったユースケースを想定しているようです。
Derived Fieldsを読むと、この機能はサーバーで定義済みのスキーマなどと組み合わせて独自のフィールドを定義できると紹介されています。
派生フィールドの定義をする
今回はUserにisRecentlyLoginというフィールドを追加してみます
schemaの記述
Relay Resolversを定義するには独自のjsdocでRelay Resolverのスキーマを記述する必要があります。
- @RelayResolver ... どのObjectにどのようなFieldを定義するか
- @rootFragment ... resolverの中で使用するfragmentの名前
以下のように記述します。
定義する関数名とフィールド名は一致している必要があるため注意してください。
/**
* @RelayResolver User.isRecentlyLogin: Boolean
* @rootFragment isRecentlyLogin_user
*
* ユーザーが最近ログインしたかどうか
*/
export const isRecentlyLogin = (): boolean | undefined => {
//
};
resolverの実装
直近のログイン日を取得するため、fragmentでlastLoginAtを取得します。
この際、readFragmentという関数を使用してfragmentのデータを参照することができます。
readFragmentはドキュメントにはrelay-runtimeから直接importできるように記述されていますが、実際にはrelay-runtime/lib/store/ResolverFragmentsからimportする必要がありました。
+import { graphql } from 'relay-runtime';
+import { readFragment } from 'relay-runtime/lib/store/ResolverFragments';
+
/**
* @RelayResolver User.isRecentlyLogin: Boolean
* @rootFragment isRecentlyLogin_user
*
* ユーザーが最近ログインしたかどうか
*/
export const isRecentlyLogin = (): boolean | undefined => {
- //
+ const node = readFragment(
+ graphql`
+ fragment isRecentlyLogin_user on User {
+ lastLoginAt
+ }
+ `,
+ );
};
fragmentを記述したら一度Relay Compilerで型を出力します。
pnpm relay
型が生成されているため、importしてresolverの第一引数の型に指定し、readFragmentの第二引数に渡します。
import { graphql } from 'relay-runtime';
import { readFragment } from 'relay-runtime/lib/store/ResolverFragments';
+import type { isRecentlyLogin_user$key } from './__generated__/isRecentlyLogin_user.graphql';
/**
* @RelayResolver User.isRecentlyLogin: Boolean
* @rootFragment isRecentlyLogin_user
*
* ユーザーが最近ログインしたかどうか
*/
export const isRecentlyLogin = (key: isRecentlyLogin_user$key): boolean | undefined => {
const node = readFragment(
graphql`
fragment isRecentlyLogin_user on User {
lastLoginAt
}
`,
+ key
);
};
今回は7日以内にログインしていればtrueを返すように実装します。
export const isRecentlyLogin = (key: isRecentlyLogin_user$key): boolean | undefined => {
const node = readFragment(
graphql`
fragment isRecentlyLogin_user on User {
lastLoginAt
}
`,
key
);
+
+ // lastLoginAtが存在しない場合はundefinedを返す
+ if (!node.lastLoginAt) {
+ return undefined;
+ }
+
+ const lastLoginDate = new Date(node.lastLoginAt);
+ const now = new Date();
+ const periodTime = 7 * 24 * 60 * 60 * 1000;
+
+ // 差分を検証
+ return now.getTime() - lastLoginDate.getTime() <= periodTime;
};
これでUser.isRecentlyLoginが使用できるようになりました。
Relayで動作検証
以下のようなコンポーネントで実際にデータを参照可能か、検証してみます。
import { graphql, useFragment } from 'react-relay';
import type { UserFragment$key } from './__generated__/UserFragment.graphql';
type Props = {
user: UserFragment$key;
}
export const User = ({ user }: Props) => {
const data = useFragment(
graphql`
fragment UserFragment on User {
lastLoginAt
isRecentlyLogin
}
`,
user
)
console.log(data);
return (
<div>
{!!data.isRecentlyLogin && <p>最近ログインしました</p>}
<p>最終ログイン日: {data.lastLoginAt ?? '-'}</p>
</div>
)
};

無事、データが表示されることが確認できました。
引数を受け取れるようにする
先ほどはシンプルな実装でしたが、実際のユースケースでは引数を受け取って動的に値の計算をしたり、fragmentに引数を渡したいことがあると思います。
そこで、periodという引数を受け取り、最近のログインとする期間を変更できるようにしてみます。
jsdocの修正
Field Argumentsによれば、以下のようにjsdocを記述することで、runtime(サーバーからデータを取得した後の処理)で引数を受け取ることができます。
受け取った引数はresolverの第二引数にobject形式で含まれます。
/**
- * @RelayResolver User.isRecentlyLogin: Boolean
+ * @RelayResolver User.isRecentlyLogin(period: Int): Boolean
* @rootFragment isRecentlyLogin_user
*
* ユーザーが最近ログインしたかどうか
*/
- export const isRecentlyLogin = (key: isRecentlyLogin_user$key): boolean | undefined => {
+ export const isRecentlyLogin = (
+ key: isRecentlyLogin_user$key,
+ args: { period?: number; }
+ ): boolean | undefined => {
const node = readFragment(
graphql`
fragment isRecentlyLogin_user on User {
lastLoginAt
}
`,
key
);
resolverの修正
args.periodを使って差分を計算するように修正します。
export const isRecentlyLogin = (
key: isRecentlyLogin_user$key,
args: { period?: number; }
): boolean | undefined => {
const node = readFragment(
graphql`
fragment isRecentlyLogin_user on User {
lastLoginAt
}
`,
key
);
// lastLoginAtが存在しない場合はundefinedを返す
if (!node.lastLoginAt) {
return undefined;
}
const lastLoginDate = new Date(node.lastLoginAt);
const now = new Date();
+ const period = args.period ?? 7;
- const periodTime = 7 * 24 * 60 * 60 * 1000;
+ const periodTime = period * 24 * 60 * 60 * 1000;
// 差分を検証
return (now.getTime() - lastLoginDate.getTime()) <= periodTime;
};
Relayで動作検証
periodを3に変更してみます。
import { UserFragment$key } from './__generated__/UserFragment.graphql';
import { graphql, useFragment } from 'react-relay';
type Props = {
user: UserFragment$key;
}
const User = ({ user }: Props) => {
const data = useFragment(
graphql`
fragment UserFragment on User {
lastLoginAt
- isRecentlyLogin
+ isRecentlyLogin(period: 3)
}
`,
user
)
console.log(data);
return (
<div>
{!!data.isRecentlyLogin && <p>最近ログインしました</p>}
<p>最終ログイン日: {data.lastLoginAt ?? '-'}</p>
</div>
)
};

無事、結果が変わることが確認できました。
まとめ
今回は簡単な実装のみの紹介となりましたが、Relay Resolversはこの他にもクライアントサイド専用のObjectの定義も可能なので、ローカルの状態管理や、サーバー側のスキーマを汚さずに特定のデータの集約などにも活用できるかと思います。
実装サンプルは以下のリポジトリからご覧いただけます。
以上!
Discussion