まくろぐ

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 クエリの呼び出し部分などが取り除かれ、表示内容だけをシンプルに記述できるようになりました。

関連記事

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