Apollo Client で始める GraphQL

この記事は [YAMAP エンジニア Advent Calender 2021](https://qiita.com/advent-calendar/2021/yamap-engginers) 5日目の記事です。


## この記事について

フロントエンドの GraphQL 導入においての学び、備忘録を共有する目的で書きました。

この記事では、Apollo Client を用いて React アプリケーションに GraphQL を導入する具体的な方法をまとめています。

## Apollo Client とは

簡単に言うと、Web フロントエンド環境（React が主で、integration により他フレームワークに対応）で GraphQL を扱うためのイケてるライブラリです。キャッシュ機構や状態管理機構も備えており、モダンで非常に理解しやすいインターフェースを有しています。

> Apollo Client is a comprehensive state management library for JavaScript that enables you to manage both local and remote data with GraphQL. Use it to fetch, cache, and modify application data, all while automatically updating your UI.

https://www.apollographql.com/docs/react/

## セットアップ

Nextjs アプリケーションの構築が出来ていることを前提で進めます。

[Nextjs の導入はこちら](https://nextjs.org/learn/basics/create-nextjs-app?utm_source=next-site&utm_medium=homepage-cta&utm_campaign=next-website)

### 開発環境

以下の開発環境を想定しています。

- エディタ
  - VSCode
- 言語
  - JavaScript または TypeScript
- フレームワーク
  - [Nextjs](https://nextjs.org/)
  - React

### ライブラリをインストール

まずは Apollo Client 関連のライブラリをインストールします。

```shell
$ yarn add @apollo/client graphql

$ yarn add -D apollo
```

### エディタ拡張機能をインストール

VSCode に [Apollo GraphQL Extension](https://marketplace.visualstudio.com/items?itemName=apollographql.vscode-apollo) をインストールします。

これによって、後述する GraphQL スキーマに基づき、クエリのコード補完がエディタ上で機能するようになります（開発効率爆上がり間違いなしですね・・）。

### Apollo の設定ファイルを作成

次に、プロジェクトルートに `apollo.cofig.js` ファイルを作成します。

```js:apollo.config.js
module.exports = {
  client: {
    includes: ['./src/**/*.js'],
    service: {
      name: 'yamap-app',
      url: 'https://api.yamap.com/graphql',
    },
  },
};
```

`client.service.url` には GrapQL API のエンドポイントを記述します。

ちなみに [Apollo Studio](https://studio.apollographql.com) というサービスもあって、これを ~~使えば幸せになれるという言い伝えがあるようです。~~ 使っている場合、service で登録している Graph の `name` を指定すれば OK のようです。

```js:apollo-config.js
module.exports = {
  client: {
    includes: ['./src/**/*.js'],
    service: 'my-graph-in-apollo-studio'
  },
};
```

https://www.apollographql.com/docs/devtools/apollo-config/#clientservice

### スキーマをダウンロード

あらかじめインストールしておいた `apollo` パッケージを使ってスキーマをダウンロードします。

```shell
$ yarn apollo service:download -c ./apollo.config.js graphql-schema.json
```

`graphql-schema.json` がプロジェクトルートに生成されたと思います。これによって、エディタ（VSCode）上でクエリのコード補完が効くようになります。

## 実装

実際に Apollo Client を使ってユーザー一覧データを取得してみます。

### `apolloClient` の実装

`@apollo/client` パッケージに含まれる `ApolloClient` クラスを使います。

```js:apollo-client.js
import { ApolloClient, InMemoryCache } from "@apollo/client";

const client = new ApolloClient({
  uri: "https://api.yamap.com/graphql",
  cache: new InMemoryCache({}),
});

export default client;
```

### `ApolloProvider` を組み込む

Nextjs アプリケーションの場合、pages 配下に `_app.js` ファイルを作成し、[Custom App](https://nextjs.org/docs/advanced-features/custom-app) を実装する必要があります。

```js:_app.js
import { ApolloProvider } from "@apollo/client";
import client from "../apollo-client"; // さっき作った apolloClient を import

function CustomApp({ Component, pageProps }) {
  return (
    <ApolloProvider client={client}>
      <Component {...pageProps} />
    </ApolloProvider>
  );
}

export default CustomApp;
```

### データの取得（CSR）

まずは `useFetch()` を使ったクライアントサイドでのデータ取得です。

#### クエリ

```js:users-query.js
import { gql } from "@apollo/client"

export const GET_USERS_QUERY = gql`
  query ListUsers($first: Int!, $after: String) {
    users(first: $first, after: $after) {
      pageInfo {
        endCursor
        hasNextPage
        hasPreviousPage
        startCursor
      }
      edges {
        cursor
        node {
          id
          databaseId
          nickname
        }
      }
    }
  }
`
```

#### クライアントサイドフェッチ

```jsx:pages/users.js
import { useQuery } from "@apollo/client"
import { GET_USERS_QUERY } from "../users-query"

function Users () {
  const { data, loading, error } = useQuery(QUERY, {
    variables: {
      first: 10,
    }
  });

  const users = data?.users.edges.map(edge => edge.node);

  if (loading) {
    return <div>Loading...</div>
  }

  if (error) {
    console.error(error);
    return null;
  }

  return (
    <div>
      <h1>ユーザー一覧</h1>
      <div>
        {users?.map(user => (
          <div key={user.id}>
            {user.nickname}
          </div>
        ))}
      </div>
    </div>
  )
}

export default Users;
```

### データの取得（SSR）

クエリは先述の `GET_USERS_QUERY` を流用します。

#### サーバーサイドフェッチ

```jsx:pages/users.js
function Users ({ initialData, initialError }) {
  const { data, loading, error } = useQuery(QUERY, {
    variables: {
      ssr: false, // getServerSideProps によって、既にサーバーサイドでデータ取得済のため
      first: 10,
    }
  });

  if (!initialData && loading) {
    return <div>Loading...</div>
  }

  if (initialError || error) {
    console.error(error);
    return null;
  }

  const users = data?.users.edges.map(edge => edge.node) || initialData?.users.edges.map(edge => edge.node);

  return (
    <div>
      <h1>ユーザー一覧</h1>
      <div>
        {users?.map(user => (
          <div key={user.id}>
            {user.nickname}
          </div>
        ))}
      </div>
    </div>
  )
}

export async function getServerSideProps() {
  const { data, error } = await client.query({
    query: QUERY,
    variables: {
      first: 10,
    }
  });

  return {
    props: {
      initialData: data,
      initialError: error,
    },
 };
```

## 以上です

ページネーションを想定しなければ SSR のコードはもう少しシンプルになるかもしれません。

参考になれば幸いです。