React プロジェクトで Apollo Client 4.0 への移行にあたり実装を最適化した話

に公開

2025/08/22 に Apollo Client 4.0 がリリースされ、移行のためのドキュメントも公開されました。
4.0 への移行にあたり、実装を最適化したので紹介します。

また、2025/09 時点では、Apollo Client 関連のユーティリティパッケージはほとんどが 4.0 に対応しておらず、この辺の移行作業についても触れていきます。

typescript-react-apollo もうダメそう

筆者のプロジェクトでは、開発を効率化するために GraphQL Codegen を使用しています。
GraphQL Codegen のプラグインとして、スキーマを React の Hooks として生成できる便利なパッケージ typescript-react-apollo を利用しています。
執筆時点でも週間80万のペースでダウンロードされている有名なパッケージですが、そもそも Hooks の生成は型のオーバーロードが多く型の安全性的に非推奨とのことで、GraphQL Codegen が提供しているプリセットの使用が強く推奨されています。
typescript-react-apollo も廃止予定とのことです。(graphql-code-generator-community より)

推奨される書き方に素直に移行する

GraphQL Codegen ではプラグインの使用を全てやめる決断をし、プリセットを使用するようにオプションを変更しました。

const config: CodegenConfig = {
  documents: "src/**/*.graphql",
  generates: {
    "src/codegen/": {
      config: {
-       dedupeFragments: true,
        enumsAsConst: true,
-       withHooks: true,
+       skipTypename: true,
      },
+     preset: "client", // 公式推奨のプリセット
+     presetConfig: {
+       fragmentMasking: false,
+     },
-     plugins: [
-       "typescript",
-       "typescript-operations",
-       "typescript-react-apollo",
-     ],
    },
    ...
  },
  overwrite: true,
  schema: ...,
};

クエリを実行する部分も Apollo Client の推奨の書き方に変更しました。
まず gql ドキュメントを定義します。

import { gql } from "@apollo/client";

const LOGIN_USER = gql`
  query LoginUser($userId: String) {
    user(userId: $userId) {
      name
      ...
    }
  }
`;

export default LOGIN_USER;

typescript-react-apollo で生成された Hooks を使用していた箇所を、useQuery・useMutation を使って書き直しました。
ジェネリクス型にはプリセットで生成された型を当て、定義した gql ドキュメントを引数とします。
これで Apollo Client, GraphQL Codegen が推奨する形の実装にできたと思われます。

-const { data } = useLoginUserQuery({
+const { data } = useQuery<
+  LoginUserQuery,
+  LoginUserQueryVariables
+>(LOGIN_USER, {
  skip: !userId,
  variables: {
    userId: userId,
  },
});

4.0に対応していないパッケージの移行

多少、苦肉の策が含まれますが、対応していないパッケージの扱いは以下のようにしました。

依存関係の問題

対応していないパッケージでは依存関係で Apollo Client 4.0 が除外されています。
この場合、取れる対応としては以下の3つとなると考えています。

  • 案1:パッケージをアンインストールし、パッケージのロジックをローカルプロジェクトで実装する、もしくはフォークして修正したパッケージをインストールする
    • デメリット:そのパッケージのアップデートを受け取れなくなる
    • デメリット:コードの解析、修正に時間がかかる
  • 案2:依存関係をオーバーライドした上でパッケージをそのまま使用し、Apollo Client に関係しないメソッドのみインポートして使用する、Apollo Client に関係する部分はローカルプロジェクトで実装する
    • デメリット:依存関係のオーバーライドをしなければならない
    • デメリット:コードの解析に時間がかかるが 案1 ほどではない
  • 案3:パッケージのアップデートを待つ
    • デメリット:依存パッケージが全てアップデートされるまで Apollo Client 4.0 への移行ができない

筆者は上記の 案2 でパッケージの移行を進めました。

具体例

筆者のプロジェクトでは、GraphQL のインフラに AWS AppSync、承認に AWS Cognito を使用しているため、ApolloLink 生成時にこれらを考慮するため以下のパッケージを使用しています。

  • aws-amplify
    • Cognito の認証周りをハンドリングするために使用
  • aws-appsync-auth-link
    • 認証情報を解析しリクエストヘッダにセットした ApolloLink を生成できる、4.0 未対応

Before
対応前は以下のようなコードとなっていました。

  1. aws-amplify の Auth インスタンスのメソッドで認証情報を取得
  2. aws-appsync-auth-link の createAuthLink メソッドに渡す
  3. 生成された ApolloLink を受け取る
  4. ApolloClient のインスタンスに ApolloLink をセットする
import { Auth } from "aws-amplify";
import { AuthLink, createAuthLink } from "aws-appsync-auth-link";

// AuthLink は ApolloLink の拡張型インスタンスです
function getAuthLink({ authType }: GetAuthLinkParams): AuthLink {
  const authLink = createAuthLink({
    auth:
      authType === "AMAZON_COGNITO_USER_POOLS"
        ? {
            jwtToken: async (): Promise<string> =>
              (await Auth.currentSession()).getIdToken().getJwtToken(),
            type: "AMAZON_COGNITO_USER_POOLS",
          }
        : {
            credentials: (): Promise<ICredentials> => Auth.currentCredentials(),
            type: "AWS_IAM",
          },
    region: ...,
    url: ...,
  });

  return authLink;
}

...

export default function getClient({ authType }: GetClientParams): ApolloClient {
  const authLink = getAuthLink({ authType });
  const client = new ApolloClient({
    cache: new InMemoryCache(),
    link: ApolloLink.from([authLink, httpLink]),
    ...
  });

  return client;
}

しかし、Apollo Client 4.0 では ApolloLink インスタンスの挙動が大幅に変更されたのと、認証情報を await で取得しているのが原因で、TypeError: Class constructor ApolloLink cannot be invoked without 'new'が発生します。
解決策としては、認証情報の取得を ApolloLink インスタンスの observer に含める必要がありますが、ApolloLink インスタンス は aws-appsync-auth-link の createAuthLink メソッドで生成されるためそれができません。
そのため、ApolloLink インスタンスを生成するプログラムはローカルプロジェクトで実装する決断をしました。つまり、不具合が起きる createAuthLink は使用せず、Apollo Client に影響しない認証情報解析系のメソッドのみ aws-appsync-auth-link から抜き出して使用する形です。

After
対応後は以下のようなコードとなりました。(変更箇所は太字)

  1. ApolloLink インスタンス を生成し、Observer を定義する
  2. 以下を Observe させる
    1. aws-amplify の Auth インスタンスのメソッドで認証情報を取得
    2. 認証情報を aws-appsync-auth-link の sign メソッドで解析する
    3. リクエストヘッダーを定義する
  3. 生成された ApolloLink を受け取る
  4. ApolloClient のインスタンスに ApolloLink をセットする
function getAuthLink({ authType }: GetAuthLinkParams): ApolloLink {
  const authLink = new ApolloLink((operation, forward) => {
    return new Observable((observer) => {
      (async (): Promise<void> => {
        try {
          if (authType === "AMAZON_COGNITO_USER_POOLS") {
            const session = await Auth.currentSession();
            const idToken = session.getIdToken().getJwtToken();
            operation.setContext({
              headers: {
                Authorization: idToken,
              },
            });
          } else {
            const credentials = await Auth.currentCredentials();
            const { accessKeyId, secretAccessKey, sessionToken } = credentials;
            const request: HttpRequest & {
              service: string;
              region: string;
              host: string;
              path: string;
              url: URL;
            } = {
              ...formatAsRequest(operation),
              host: url.host,
              path: url.pathname,
              region: ...,
              service: "appsync",
              url,
            };
            operation.setContext({
              headers: {
                Authorization: Signer.sign(request, {
                  access_key: accessKeyId,
                  secret_key: secretAccessKey,
                  session_token: sessionToken,
                }),
              },
            });
          }

          const observable = forward(operation);
          observable.subscribe({
            complete: observer.complete.bind(observer),
            error: observer.error.bind(observer),
            next: observer.next.bind(observer),
          });
        } catch (error) {
          observer.error(error);
        }
      })();
    });
  });

  return authLink;
}

...

export default function getClient({ authType }: GetClientParams): ApolloClient {
  const authLink = getAuthLink({ authType });
  const client = new ApolloClient({
    cache: new InMemoryCache(),
    defaultOptions: {
      watchQuery: {
        fetchPolicy: "network-only",
      },
    },
    link: ApolloLink.from([authLink, httpLink]),
  });

  return client;
}

Discussion