React プロジェクトで Apollo Client 4.0 への移行にあたり実装を最適化した話
2025/08/22 に Apollo Client 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 への移行ができない
筆者は上記の 案1 でパッケージの移行を進めた。
具体例
筆者のプロジェクトでは、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 で取得しているのが原因で、createAuthLink メソッドでTypeError: Class constructor ApolloLink cannot be invoked without 'new'
が発生してしまった。
解決策としては、認証情報の取得を ApolloLink インスタンスの observer に含める必要があるが、ApolloLink インスタンス は aws-appsync-auth-link の createAuthLink メソッドで生成されるためそれができない。
そのため、aws-appsync-auth-link の使用は廃止し、ローカルプロジェクトでApolloLink インスタンスの生成をするようにし、署名ロジックを実装した。
After
対応後は以下のようなコードとなった。(変更箇所は太字)
- ApolloLink インスタンス を生成し、Observer を定義する
-
以下を Observe させる
- aws-amplify の Auth インスタンスのメソッドで認証情報を取得
- 署名V4パッケージを使用し署名する
- リクエストヘッダーを定義する
- 生成された 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;
// NOTE: 署名V4を生成する
const signatureV4 = new SignatureV4({
credentials: {
accessKeyId,
secretAccessKey,
sessionToken,
},
region: process.env.NEXT_PUBLIC_AWS_PROJECT_REGION || "",
service: "appsync",
sha256: Sha256,
});
const url = new URL(
process.env.NEXT_PUBLIC_AWS_APPSYNC_GRAPHQL_ENDPOINT || "",
);
// NOTE: 実際に送られる GraphQL リクエストボディを署名用に生成する
const { operationName, query, variables } = operation;
// NOTE: プロパティの順番を担保しなければならないので Linter を無視する
/* eslint-disable */
const body = JSON.stringify({
operationName: operationName,
variables: variables || {},
extensions: {
clientLibrary: { name: "@apollo/client", version: version },
},
query: print(query),
});
/* eslint-enable */
const httpRequest = new HttpRequest({
body,
headers: {
accept: "*/*",
"content-type": "application/json; charset=UTF-8",
host: url.hostname,
},
hostname: url.hostname,
method: "POST",
path: url.pathname,
});
const signedRequest = await signatureV4.sign(httpRequest);
operation.setContext({
headers: {
accept: "*/*",
authorization: signedRequest.headers.authorization,
"content-type": "application/json; charset=UTF-8",
"x-amz-content-sha256":
signedRequest.headers["x-amz-content-sha256"],
"x-amz-date": signedRequest.headers["x-amz-date"],
"x-amz-security-token":
signedRequest.headers["x-amz-security-token"],
"x-amz-user-agent": "aws-amplify",
},
});
}
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