まくろぐ

GraphQL のクエリの一部をフラグメント化して再利用する (Fragments)

更新:
作成:

GraphQL のフラグメントを定義する

GraphQL クエリの中で、同じようなフィールドの指定(選択セット)を複数回使用する場合、それを フラグメント (Fragment) という再利用可能な選択セットとして切り出して定義しておくことができます。

例えば次の GraphQL クエリは、GitHub から自分のユーザー情報 (viewer) と、特定のユーザーの情報 (user) を一度に取得しています。

GraphQL クエリ
query QueryTwoUsers {
  viewer {
    login       # ログインID
    name        # ユーザー名
    url         # ユーザーの GitHub ホームページ
    websiteUrl  # ユーザーの Web サイト
    avatarUrl   # ユーザーのアバター画像
  }

  user(login: "ログインID") {
    login
    name
    url
    websiteUrl
    avatarUrl
  }
}

viewer フィールドと user フィールドは、両方とも User 型 のフィールドで、しかも、上記の例では User オブジェクトの中の同じフィールドを参照しています。 明らかに冗長な書き方です。

このようなケースでは、あるオブジェクトの特定のフィールドを参照するための選択セット (selection set) を、フラグメントの形で定義することができます。 次の例では、User オブジェクトの特定のフィールドを選択するための userFragment というフラグメントを定義しています。 フラグメントを使用する場所では、...userFragment のようにドットを 3 つ付けて参照します(JavaScript のスプレッド構文と同じです)。

GraphQL クエリ
query QueryTwoUsers {
  viewer {
    ...userFragment
  }

  user(login: "ログインID") {
    ...userFragment
  }
}

fragment userFragment on User {
  login       # ログインID
  name        # ユーザー名
  url         # ユーザーの GitHub ホームページ
  websiteUrl  # ユーザーの Web サイト
  avatarUrl   # ユーザーのアバター画像
}

fragment の定義は、query の定義と同じ階層に記述することに注意してください。 また、on User というのは、User オブジェクトのフィールドを選択するフラグメントであることを示しており、このフラグメントは User オブジェクトのフィールド部分でしか使えません。 上記の例では、vieweruserUser 型のフィールドなので、その中で問題なく ...userFragment と参照できています。

上記のサンプルコードはあまり実用的じゃなかったので、もう少し本物のアプリで使いそうなクエリを載せておきます。 次の GraphQL クエリでは、ログイン中のユーザー情報と、そのユーザーの Issue 情報を取得しています。 Issue がアサインされているユーザーの情報を取得するために、userFragment フラグメントを使っています。

GraphQL クエリ
query QueryLoginUser {
  viewer {
    ...userFragment
    issues(states: OPEN, last: 10) {
      nodes {
        id
        title
        assignees(last: 10) {
          nodes { ...userFragment }
        }
      }
    }
  }
}

fragment userFragment on User {
  login name url websiteUrl avatarUrl
}

Apollo Client で GraphQL のフラグメントを使用する

React を使った Web アプリでは、GraphQL ライブラリとして Apollo Client が使用されることが多いと思います。 もちろん、Apollo Client で使用する GraphQL クエリの中でもフラグメントを使用できます。

TypeScript (JavaScript) では、クエリ文字列の中から変数展開することができるので、フラグメントの定義は文字列変数(定数)の形で定義しておくと便利です。 次の例では、rateLimitFragment という GraphQL フラグメントを、RATE_LIMIT_FRAGMENT という文字列定数として定義しています。

queries.ts
import {gql} from '@apollo/client';

const RATE_LIMIT_FRAGMENT = gql`
  fragment rateLimitFragment on Query {
    rateLimit {
      cost
      remaining
    }
  }
`;

// 自分にアサインされた PullRequest の一覧
export const QUERY_MY_PULLS = gql`
  ${RATE_LIMIT_FRAGMENT}
  query {
    ...rateLimitFragment
    rateLimit { cost remaining }
    search(type: ISSUE, last: 100, query: "is:open is:pr review-requested:@me") {
      issueCount
      nodes {
        ... on PullRequest { number title bodyText url }
      }
    }
  }
`;

文字列定数 RATE_LIMIT_FRAGMENT で定義されたフラグメントを使用するために、QUERY_MY_PULLS の中で ${RATE_LIMIT_FRAGMENT} と展開しています。 つまり、そこに fragment の定義を記述したのと同じ振る舞いになります。 あとは適切な場所で ...rateLimitFragment と参照すれば OK です。

このようにフラグメント定義部分を文字列定数として切り出しておけば、別の GraphQL クエリの中からも同様に参照することができます。

一応、QUERY_MY_PULLS の使用例も。

components/MyPullRequests.tsx
import * as React from 'react';
import {useQuery} from '@apollo/client';
import {PullRequestLink} from './PullRequestLink';
import {QUERY_MY_PULLS} from '../queries';

export const MyPullRequests: React.FC = () => {
  const {loading, error, data} = useQuery(QUERY_MY_PULLS);
  if (loading) return <p>Loading ...</p>;
  if (error) return <p>Error: {error.message}</p>;
  const pulls = createPullRequests(data);
  return <>
    <h3>自分のレビュー待ち PR</h3>
    <ul>
      {pulls.map(x => <PullRequestLink key={x.number} pr={x}/>)}
    </ul>
  </>;
};

// ...

関連記事

Apollo Client の useQuery 呼び出し部分をカスタムフックで分離する

更新:
作成:

Apollo Client で GraphQL クエリを実行するときは、カスタムフックとして useQuery 関数の呼び出し部分を抽出すると、コンポーネント側のコードをシンプルにすることができます。

次のサンプルコードでは、GraphQL クエリで GitHub のログインユーザー情報を取得して表示する Viewer コンポーネントを実装しています。 GraphQL のクエリ呼び出し部分や、取得したデータを IViewer オブジェクトに詰める部分などが混在しており、あまり整理されているとは言えません。

components/Viewer.tsx
import * as React from 'react';
import {Link} from 'react-router-dom';
import {gql, useQuery} from '@apollo/client';
import style from './Viewer.scss';

const GET_VIEWER = gql`
  query {
    viewer {
      login      # ログインID
      url        # ホームページのURL
      avatarUrl  # アバター画像のURL
    }
  }
`;

interface IViewer {
  /** ログインID */
  login: string;
  /** ホームページのURL */
  url: string;
  /** アバター画像のURL */
  avatarUrl: string;
}

/** 「ロード中メッセージ」を表示するコンポーネント */
const LoadingComponent: React.FC = () => {
  return <p>Loading ...</p>;
}

/** 「エラーメッセージ」を表示するコンポーネント */
const ErrorComponent: React.FC<{message: string}> = ({message}) => {
  return <p style={{color: 'red'}}>{message}</p>;
}

/** 「ユーザー情報」を表示するコンポーネント */
export const Viewer: React.FC = () => {
  const {loading, error, data} = useQuery(GET_VIEWER);
  if (loading) return <LoadingComponent />;
  if (error) return <ErrorComponent message={error.message} />;
  const viewer: IViewer = data.viewer;

  return <div className={style.container}>
    <Link to="/signout" className={style.signout}>サインアウト</Link>
    <a href={viewer.url}><img src={viewer.avatarUrl}/></a>
  </div>;
};

そこで、次のように useQuery 呼び出し部分を、独自の useViewer 関数でラップしてやります。 このように、関数内部で useXxx 系のフック関数を呼び出すものをカスタムフックと呼びます。 カスタムフックは、通常のフック関数と同様の制約を引き継ぎます(関数コンポーネント内で一定の順序で呼び出さないといけないなど)。

hooks.tsx(カスタムフック)
import {gql, useQuery, QueryResult} from '@apollo/client';

const GET_VIEWER = gql`
  query {
    viewer {
      login      # ログインID
      url        # ユーザーのホームページ
      avatarUrl  # ユーザーのアバター画像
    }
  }
`;

/**
 * useViewer フックの戻り値の data プロパティの型。
 */
export interface IViewer {
  /** ログインID */
  login: string;
  /** ホームページのURL */
  url: string;
  /** アバター画像のURL */
  avatarUrl: string;
};

/**
 * GitHub でログイン中のユーザー情報を取得します。
 */
export function useViewer(): QueryResult<IViewer> {
  const result = useQuery(GET_VIEWER);
  if (!result.data) return result;

  // result.data をパースして IViewer オブジェクトとして data プロパティに詰めなおす
  const newData: IViewer = result.data.viewer;
  return {...result, data: newData};
}

useViewer 関数は、データ取得が完了するまでは useQuery 関数の戻り値をそのまま返すので、呼び出し側は useQuery 関数を使った場合と同様にローディング中の処理やエラー処理を記述することができます。

const {loading, error, data /* IViewer */} = useViewer();

ポイントは、データ取得が完了したときの処理で、戻り値の data プロパティに IViewer 型のオブジェクトを詰めて返すようにしています。 これにより、useViewer の呼び出し側では、data プロパティを型付けされた IViewer オブジェクトとして参照することができます。

☝️ QueryResult インタフェース QueryResult インタフェースは、Apollo Client が定義している useQuery の戻り値の型です。 QueryResult オブジェクトの data プロパティは、デフォルトでは any 型ですが、QueryResult<IViewer> のようにタイプパラメータを指定すると、data プロパティを IViewer 型として参照できるようになります。 上記のサンプルコードでは、この仕組みを使って data プロパティを IViewer オブジェクトの格納先として再利用(値を上書き)しています。 もちろん QueryResult をそのまま使わずに、独自のリザルト型を定義しても構いません。

上記の独自フック関数 useViewer は、次のような感じで利用します。

components/Viewer.tsx(リファクタ後)
import * as React from 'react';
import {Link} from 'react-router-dom';
import {useViewer} from '../hooks';
import style from './Viewer.scss';

const LoadingComponent: React.FC = () => {
  return <p>Loading ...</p>;
}

const ErrorComponent: React.FC<{message: string}> = ({message}) => {
  return <p style={{color: 'red'}}>{message}</p>;
}

export const Viewer: React.FC = () => {
  const {loading, error, data /* IViewer */} = useViewer();
  if (loading) return <LoadingComponent />;
  if (error) return <ErrorComponent message={error.message} />;

  return <div className={style.container}>
    <Link to="/signout" className={style.signout}>サインアウト</Link>
    <a href={data.url}><img src={data.avatarUrl}/></a>
  </div>;
};

Viewer コンポーネントから GraphQL クエリの呼び出し部分などが取り除かれ、表示内容だけをシンプルに記述できるようになりました。

関連記事

JavaScript で任意のテキストをクリップボードにコピーする

更新:
作成:

copyToClipboard 関数

次の copyToClipboard 関数を使うと、引数で指定したテキストを OS のクリップボードにコピーすることができます。

function copyToClipboard(text){
  // テキストコピー用の一時要素を作成
  const pre = document.createElement('pre');

  // テキストを選択可能にしてテキストセット
  pre.style.webkitUserSelect = 'auto';
  pre.style.userSelect = 'auto';
  pre.textContent = text;

  // 要素を追加、選択してクリップボードにコピー
  document.body.appendChild(pre);
  document.getSelection().selectAllChildren(pre);
  const result = document.execCommand('copy');

  // 要素を削除
  document.body.removeChild(pre);

  return result;
}

JavaScript からクリップボードにテキストをコピーするときは、任意の HTML 要素のテキストを選択して、document.execCommand('copy') を実行するという流れになります。 そのため、上記の関数では、テキスト選択用の一時的な pre 要素を作成しています。

使用例

例えば次のようにすると、ボタンを押したときにクリップボードにテキストをコピーできます。

← 実際に動作します

sample.html
<button id="copy">クリップボードにコピー</button>

<script>
window.addEventListener('DOMContentLoaded', () => {
  document.getElementById('copy').addEventListener('click', () => {
    copyToClipboard('こんにちは!\nテキストがコピーされたよ!');
  });
});
</script>

関連記事

メニュー

まくろぐ
サイトマップまくへのメッセージ