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
対応前は以下のようなコードとなっていました。
- aws-amplify の Auth インスタンスのメソッドで認証情報を取得
- aws-appsync-auth-link の createAuthLink メソッドに渡す
- 生成された ApolloLink を受け取る
- 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
対応後は以下のようなコードとなりました。(変更箇所は太字)
- ApolloLink インスタンス を生成し、Observer を定義する
-
以下を Observe させる
- aws-amplify の Auth インスタンスのメソッドで認証情報を取得
- 認証情報を aws-appsync-auth-link の sign メソッドで解析する
- リクエストヘッダーを定義する
- 生成された ApolloLink を受け取る
- 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