📌

GraphQLクライアントとNuxtとの組み合わせ

2023/03/12に公開

主な役割

GraphQLクライアントは、クライアント側で動作するプログラムです。主な役割は、クライアント側からGraphQLサーバーにクエリをリクエストし、そのレスポンスを受け取ることです。

queryの構築とレスポンス処理

GraphQLクライアントは、関連するヘッダーとコンテキストの情報、そしてGraphQLドキュメントのみを使用して、完全なqueryを構築することができる。
専用のクライアントを使用せずにFetch APIを使ってGraphQLサーバーと通信することができますが、ライブラリを使用するとFetch API呼び出しのコードを毎回記述する代わりに、クライアントが呼び出しを処理し、パースした後にレスポンスデータやエラーを返すことができる。

UIステートの管理

GraphQLはUIステートを管理し、複数のUIコンポーネント間でデータを同期するのにも役立つ。

キャッシュの更新

GraphQLクライアントは、queryやmutationから取得したデータのキャッシュエントリの管理にも使用することができます。このようなUIのリアクティブな更新は、キャッシュを使用することで実現することができます。

クライアントの種類

Apollo Client

Apolloが開発したGraphQLクライアントは、おそらく最も一般的に使用されています。また、Nuxtのモジュールも提供されています。
このクライアントは、ローカルおよびリモートのデータをGraphQLで管理できる、包括的な状態管理GraphQLクライアントで、全機能が備わっています。

https://nuxt.com/modules/apollo#undefined
https://www.apollographql.com/docs/react/why-apollo/

長所

  • キャッシュと状態管理ライブラリの機能を兼ね備えていて、Apollo Clientには、接続するGraphQL APIの有無にかかわらず、ローカルの状態を管理するためのキャッシュおよび状態管理機能が内蔵されています。リモートAPIから受信した状態をキャッシュし、ローカルの状態と組み合わせることで、データを扱う際のネットワーク帯域と時間を節約することができる。
  • 多くの組み込み機能が搭載されています。エラー処理、ページネーション、データのプリフェッチ、React Hooksとの統合などの機能が組み込まれている。
  • 複数のフレームワークに対応。

短所

  • 最大のデメリットはバンドルサイズ。Apolloを使えるようにした最小バージョンが30.7KBで、URQLなど他のGraphQLクライアントの役2倍のサイズとなる
  • React以外の広域なサポートがない。Vueなどを使用する場合はサードパーティツールに依存しないといけない。
  • 未解決のissueが多い。

Vue/Nuxt3対応状況

vue3は@vue/apollo-composableというライブラリがあるがbeta版ぽい
Nuxt3もNuxtApolloというのを提供しているが、こちらはalpha版
※2022/12 時点

どちらもサードパーティライブラリでissueも結構多そう

Urql

軽量でカスタマイズ性と拡張性の高いGraphQLクライアント
https://formidable.com/open-source/urql/

長所

  • 軽量でセットアップが簡単。バンドルサイズが12KBで出荷されており、特にApolloなどの他のGraphQLクライアントと比較すると信じられないほど軽量
  • 柔軟で拡張可能。開発者が URQL GraphQL ライブラリをニーズに合わせてカスタマイズできるように、拡張API を提供している。この拡張性と柔軟性により、開発者は URQL の上にカスタム実装を構築し、カスタムシナリオに合うように微調整することができる
  • キャッシュが賢い。Document Cachingという方式でキャッシュしている
  • オフラインモードやファイルアップロードなどの機能をファーストクラスでサポートしている
  • ドキュメントが短くわかりやすい

短所

  • 他のクライアントと比べて比較的新しくコミュニティが小さい
  • バッチクエリ、ローカルの状態管理といった機能がファーストクラスサポートされていない

Vue/Nuxt3対応状況

Vue3は公式で@urql/vueというのを提供している
Nuxtとのインテグレーションは提供していないっぽい

graphql-request

graphql-requestは最もミニマルでシンプルなGraphQLクライアント。小さなスクリプトやシンプルなアプリに最適。

ApolloやRelayのようなGraphQLクライアントと比較すると、graphql-requestはキャッシュを内蔵しておらず、フロントエンドフレームワークの統合もされていない。パッケージとAPIを可能な限り最小化することが目的。

キャッシュ機能が必要ないならこれでよさそう。

https://github.com/prisma-labs/graphql-request

vue3/Nuxt3対応状況

公式ではフレームワークとの統合はしてないとあるが、サードパーティでNuxt GraphQL Clientが提供されている

Nuxt3で動作確認

NuxtApollo × GraphQL Code Generator

NuxtApolloをドキュメントの手順にそってインストールする

nuxt.config.ts

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  modules: ['@nuxtjs/apollo'],
  apollo: {
    clients: {
      default: {
        httpEndpoint: 'http://localhost:3300/graphql'
      }
    },
  }
})

Graphql Code Generator を手順に沿ってインストール

codegen.ts

import type { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
  overwrite: true,
  schema: "http://localhost:3300/graphql",
  documents: "app.vue",
  ignoreNoDocuments: true,
  generates: {
    ".types/gql/": {
      preset: "client",
      plugins: [],
      config: {
        useTypeImports: true
      },
    }
  }
};

export default config;

GraphQLのドキュメントファイルを作成
users.graphql

query getUsers {
  users {
    age
    id
    name
  }
}

mutation CreateUser {
  createUser(newUser: { name: "test", age: 28 })
}

型生成
npm run codegen

.types/gql/graphql.ts

/* eslint-disable */
import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: string;
  String: string;
  Boolean: boolean;
  Int: number;
  Float: number;
  /** A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. */
  DateTime: any;
};

export type Mutation = {
  __typename?: 'Mutation';
  createUser: Scalars['String'];
  removeUser: Scalars['Boolean'];
};


export type MutationCreateUserArgs = {
  newUser: NewUser;
};


export type MutationRemoveUserArgs = {
  id: Scalars['Int'];
};

export type NewUser = {
  age: Scalars['Int'];
  name: Scalars['String'];
};

export type Query = {
  __typename?: 'Query';
  users?: Maybe<Array<UserModel>>;
};

export type UserModel = {
  __typename?: 'UserModel';
  age: Scalars['Int'];
  createdAt: Scalars['DateTime'];
  id: Scalars['ID'];
  name: Scalars['String'];
};

export type GetUsersQueryVariables = Exact<{ [key: string]: never; }>;


export type GetUsersQuery = { __typename?: 'Query', users?: Array<{ __typename?: 'UserModel', age: number, id: string, name: string }> | null };

export type CreateUserMutationVariables = Exact<{ [key: string]: never; }>;


export type CreateUserMutation = { __typename?: 'Mutation', createUser: string };


export const GetUsersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"getUsers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"users"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"age"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode<GetUsersQuery, GetUsersQueryVariables>;
export const CreateUserDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createUser"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"newUser"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"name"},"value":{"kind":"StringValue","value":"test","block":false}},{"kind":"ObjectField","name":{"kind":"Name","value":"age"},"value":{"kind":"IntValue","value":"28"}}]}}]}]}}]} as unknown as DocumentNode<CreateUserMutation, CreateUserMutationVariables>;

実際に使ってみる
app.vue

<script lang="ts" setup>
import { GetUsersDocument, CreateUserDocument } from '.types/gql/graphql';

const { result } = useQuery(GetUsersDocument)
result.value?.users?.map((u) => {
  u.age
})

const { mutate } = useMutation(CreateUserDocument)
const newUser = await mutate()

</script>

<template>
  <div>
    <p>{{ result }}</p>
    <p>{{ newUser }}</p>
  </div>
</template>

Urql × GraphQL Code Generator

@urql/vueを手順にそってインストール

nuxt.config.tsではなくplugins/urqlClient.ts

import {
  install,
  createClient,
  debugExchange,
  dedupExchange,
  fetchExchange
} from "@urql/vue";

export default defineNuxtPlugin((nuxt) => {
  const apiURL = "http://localhost:3300/graphql";

  const client = createClient({
    url: apiURL,
    requestPolicy: "network-only",
    exchanges: [debugExchange, dedupExchange, fetchExchange]
  });

  install(nuxt.vueApp, client);
});

他はApolloと手順は変わらず

<script lang="ts" setup>
import { useMutation, useQuery } from '@urql/vue';
import { GetUsersDocument, CreateUserDocument } from '.types/gql/graphql';

const { data } = useQuery({ query: GetUsersDocument })

data.value?.users?.map((u) => {
  u.name
})

useMutation(CreateUserDocument)

</script>

<template>
  <div>
    <p>{{ data }}</p>
  </div>
</template>

Nuxt GraphQL Client(graphql-request + GraphQL Code Generator)

デフォルトでGraphQL Code Generatorが含まれている
nuxt.config.ts

export default defineNuxtConfig({
  modules: ['nuxt-graphql-client'],

  runtimeConfig: {
    public: {
      GQL_HOST: 'http://localhost:3300/graphql' // overwritten by process.env.GQL_HOST
    }
  }
})

codegen.tsは不要でGraphQLのドキュメントファイルだけ作成

query getUsers {
  users {
    age
    id
    name
  }
}

mutation CreateUser {
  createUser(newUser: { name: "test", age: 28 })
}

npm run devで起動すると一緒に型も生成してくれる

app.vue

<script lang="ts" setup>
const { users } = await GqlGetUsers()

users!.map((u) => {
  u.name
})

const { createUser } = await GqlCreateUser()

</script>

<template>
  <div>
    <p>{{ users }}</p>
    <p>{{ createUser }}</p>
  </div>
</template>

関数も生成してくれて、インポートなしで使用できる

Discussion