NRIネットコム Blog

NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

Next.js (App Router) + Apollo ClientでのGraphQLクライアント実装

本記事は  Reactウィーク  5日目の記事です。
📍  4日目  ▶▶ 本記事   📱

はじめに

こんにちは、NRIネットコムの辻下です。普段はアプリ担当エンジニアとして仕事をしています。(最近フロントエンドにも挑戦中...)
最近業務でNext.js (App Router) + Apollo ClientでWebアプリケーションでのデータフェッチを実装する機会がありましたので、今回はその実装方法について手短に書いていこうと思います。

利用するツール

Next.js (App Router)

言わずと知れたReactのフレームワークです。今回はApp Routerのバージョンでの実装になります。 nextjs.org

Apollo Client

GraphQLのクライアントを実装するライブラリです。 www.apollographql.com

  • Next.js App Routerで利用するためには別途下記プラグインも導入が必要です。

@apollo/experimental-nextjs-app-support - npm

  • こちらの利用方法については公式のブログポストがあります。今回の記事でもApolloの設定部分についてこの記事を参考にしています。

How to use Apollo Client with Next.js 13 | Apollo GraphQL Blog

GraphQL-Codegen

GraphQLクエリの実行部分のコードや型定義を自動生成してくれるツールです。 the-guild.dev

前提

Next.jsアプリ、およびGraphQLバックエンドは構築済みとし今回は説明を省かせていただきます。

実装

型定義コードの自動生成

GraphQL-Codegenを利用して、型定義のコードを自動生成します。

  • graphql-codegenと必要なプラグインをnpm install
npm install --save-dev @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations
import { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
    overwrite: true,
    schema: ['graphql/*.graphql'], // バックエンドgraphql schema定義ファイルを参照 (*1)
    documents: ['src/**/*.graphql'], // クライアント側で実行するgralhqlクエリを記載したファイルを参照 (*2)
    generates: {
        'src/generated/components.ts': {
            // 出力先のディレクトリとファイル名を設定 (*3)
            plugins: ['typescript', 'typescript-operations'],
        },
    },
    config: {
        namingConvention: {
            enumValues: 'change-case-all#upperCase',
        },
    },
};

export default config;
  • package.jsonにnpm scriptとしてコマンド登録
{
    "scripts": {
        "graphql:generate": "graphql-codegen --config scripts/graphql/codegen.ts"
    }
}
  • 実行
npm run graphql:generate
  • *3で設定した出力先にcomponents.tsファイルが出力されていたらOK。

補足

*1 で設定した、バックエンドGraphQLスキーマ定義ファイル

  • バックエンド側で定義したスキーマファイルのパスを設定します。
    • 実際のPJではgit submoduleでバックエンド側のソースをcloneしてきて、そこのschema.graphqlのファイルパスを指定する形で運用していました。
    • 中身はこちらの記事のようなものになります。(バックエンド側も要勉強...)

*2 で設定した、クライアント側で実行するgralhqlクエリを記載したファイル例

  • query
query GetProducts {
   getProducts {
       productId
       productName
    }
}
  • mutation
mutation AddProduct($productName: String!) {
    createProduct(productName: $productName)
}

Apollo Clientの設定

  • Apollo Clientをnpm install
npm install --save @apollo/client @apollo/experimental-nextjs-app-support
  • Apollo Clientの設定
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { onError } from '@apollo/client/link/error';
import { registerApolloClient } from '@apollo/experimental-nextjs-app-support';

export const { getClient } = registerApolloClient(async () => {
    return new ApolloClient({
        cache: new InMemoryCache(),
        link: getLink(),
    });
});

const getLink = () => {
    const httpLink = new HttpLink({
        uri: process.env.API_ENDPOINT_URI,
        fetchOptions: { cache: 'no-store' },
    });

    const authLink = setContext(async (_, { headers }) => {
        // Bearer認証など
        return {
            headers: {
                ...headers,
                authorization: 'Bearer XXX',
            },
        };
    });

    const redirectLink = onError(({ graphQLErrors, networkError }) => {
        // エラー監視など
        if (graphQLErrors)
            graphQLErrors.forEach(({ message, locations, path }) =>
                console.error(
                    `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
                ),
            );
        if (networkError) {
            console.error(`[Network error]: ${networkError}`);
        }
    });

    return authLink.concat(redirectLink).concat(httpLink);
};

実際にクエリを実行してみる

Query

action.ts

'use server';

import { getClient } from '@/configs/ApolloClient';
import { GetProductsDocument, GetProductsQuery } from '@/generated/components'; // 自動生成した型を使用

export const getProducts = async () => {
    const getClientFunc = await getClient();
    const { data } = await getClientFunc.query<GetProductsQuery>({
        query: GetProductsDocument,
    });

    return data?.getProducts;
};

MyQueryPage.tsx

import { getProducts } from './action';

export const MyQueryPage = async () => {
    const products = await getProducts();

    return (
        <main>
            {products &&
                products.map((product) => (
                    <div
                        key={product.productId}
                    >{`${product.productId} : ${product.productName}`}</div>
                ))}
        </main>
    );
};
  • Server ComponentからGraphQL APIを実施しています。
  • GraphQL-Codegenで自動生成した型定義、クエリ文を使って実装しているのでスッキリ書けます。

Mutation

action.ts

'use server';

import { getClient } from '@/configs/ApolloClient';
import { AddProductDocument, AddProductMutation } from '@/generated/components'; // 自動生成した型を使用
import { revalidatePath } from 'next/cache';

export const addProduct = async (productName: string) => {
    const client = await getClient();
    await client.mutate<AddProductMutation>({
        mutation: AddProductDocument,
        variables: { productName },
    });
    revalidatePath('/products'); // Mutation実行後のデータの内容で画面を再レンダリング
};

MyMutationComponent.tsx

'use client';

import { useTransition } from 'react';
import { addProduct } from './action';

export const MyMutationComponent = () => {
    const [_, startTransition] = useTransition();

    const handleClick = () => {
        startTransition(async () => {
            await addProduct();
        });
    };

    return <button onClick={handleClick}>Add Product</button>;
};
  • Mutationについては、登録ボタン押下で登録処理を実行など、Client Componentから実行するケースも多いと思い、Client Componentでの実行例としています。
  • GraphQL API実行後、revalidatePath 関数を実行することで、Mutation実行後のデータの内容で画面を再レンダリングできます。

この構成にして良かったこと

Apollo ClientはGraphQLクライアントのライブラリとして一般的によく利用されていますし、状態管理機能など、今後の機能追加にも耐えうる多くの機能を備えたライブラリです。 またApp Routerについても、今後Next.jsではメインでメンテされていくバージョンになります。 今回実装したシステムは今後の機能追加を前提としたプロジェクトだったため、将来的な機能追加、保守に耐えうる構成とすることができて良かったです。

まとめ

Apollo ClientをNext.js App Routerで使う場合の日本語の記事が少なく、少し調査に苦労したためこの機会に記事を書かせていただきました。 お役に立てれば幸いです!