Next.js 14集成Apollo GraphQL遇频繁刷新及上下文错误求助
问题描述
在Next.js 14项目中引入ApolloProvider后,页面出现频繁刷新,并抛出以下错误:
Unhandled Runtime Error
Error: Context is not available in this environment.
我正在尝试为Next.js 14配置Apollo Client GraphQL,请问是否需要使用实验包来适配App Router?
相关代码
1. ApolloProvider包装组件
'use client' import { ApolloProvider } from "@apollo/client"; import client from "../../../apollo"; export default function ApolloProviderComponent({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( <ApolloProvider client={client}> <div className="max-[90vw] px-[30px] py-[30px]"> {children} </div> </ApolloProvider> ); }
2. Apollo Client配置文件
/* 1. Creating connection with GraphQL 2. Handling and showing GraphQL and network errors. */ // packages block import { onError } from "@apollo/client/link/error"; import { ApolloClient, InMemoryCache, ApolloLink, HttpLink, from, Operation, NextLink } from "@apollo/client"; // utils and constants block import { signOut, useSession } from "next-auth/react"; import { CONFLICT_EXCEPTION, INVALID_AUTH_TOKEN, INVALID_OR_EXPIRED_TOKEN_MESSAGE, NOT_FOUND_EXCEPTION, PRECONDITION_FAILED_EXCEPTION, REQUEST_NOT_FOUND, TOKEN_INVALID, TOKEN_NOT_FOUND, UNAUTHENTICATED, UNAUTHORIZED } from "@/app/constants"; const handleLogout = () => { signOut() } const authMiddleware = new ApolloLink((operation: Operation, forward: NextLink) => { const { data: session } = useSession() let token if (session) { token = session?.user?.data?.login?.accessToken } operation.setContext({ headers: { authorization: `Bearer ${token}`, }, }); return forward(operation); }); const httpLink = new HttpLink({ uri: `${process.env.REACT_APP_API_BASE_URL}/graphql` }); const errorLink = onError(({ graphQLErrors, networkError }: any) => { if (graphQLErrors) { graphQLErrors.map( ({ extensions }: any) => { if (extensions) { const { exception, code } = extensions; if (code === UNAUTHENTICATED) handleLogout() if (exception) { const { response } = exception as any if (response) { const { error, response: errorResponse } = response if (error && error !== REQUEST_NOT_FOUND && error !== NOT_FOUND_EXCEPTION && error !== CONFLICT_EXCEPTION) { if (error === TOKEN_NOT_FOUND) { console.log(INVALID_OR_EXPIRED_TOKEN_MESSAGE) } else console.log(error) } if (errorResponse) { const { error: responseError, message } = errorResponse; if (message && message !== REQUEST_NOT_FOUND && message !== NOT_FOUND_EXCEPTION && message !== CONFLICT_EXCEPTION) { console.log(message) } else if (responseError && responseError !== REQUEST_NOT_FOUND && responseError !== NOT_FOUND_EXCEPTION && responseError !== CONFLICT_EXCEPTION && responseError === PRECONDITION_FAILED_EXCEPTION) { console.log(responseError) } } } } } return null } ); const [{ message }] = graphQLErrors; if (message === UNAUTHORIZED || message === TOKEN_INVALID || message === INVALID_AUTH_TOKEN) { } } if (networkError) console.log(networkError.message) }); const client = new ApolloClient({ cache: new InMemoryCache(), connectToDevTools: true, link: from([authMiddleware, errorLink, httpLink]), }); export default client;
3. RootLayout文件
import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; import Provider from "./Provider"; import AppBar from "./components/AppBar"; import { ApolloProvider } from "@apollo/client"; import client from "../../apollo"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { title: "Create Next App", description: "Generated by create next app", }; export default async function RootLayout({ children }: Readonly<{ children: React.ReactNode; }>) { return ( <html lang="en"> <body className={inter.className}> <ApolloProvider client={client}> <Provider> <AppBar /> {children} </Provider> </ApolloProvider> </body> </html> ); }
解决方案
核心结论
不需要使用实验包,官方@apollo/client已完全支持Next.js 14 App Router。问题根源是未正确区分服务器组件与客户端组件的边界,导致客户端钩子在服务器环境执行,引发上下文错误和页面刷新。
具体修复步骤
1. 修正RootLayout的组件渲染逻辑
RootLayout默认是服务器组件,不能直接渲染ApolloProvider(客户端组件)。需改用你已创建的带'use client'指令的ApolloProviderComponent:
import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; import Provider from "./Provider"; import AppBar from "./components/AppBar"; import ApolloProviderComponent from "./components/ApolloProviderComponent"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { title: "Create Next App", description: "Generated by create next app", }; export default async function RootLayout({ children }: Readonly<{ children: React.ReactNode; }>) { return ( <html lang="en"> <body className={inter.className}> <ApolloProviderComponent> <Provider> <AppBar /> {children} </Provider> </ApolloProviderComponent> </body> </html> ); }
2. 修复Apollo Client中的authMiddleware
useSession是客户端钩子,不能在服务器环境执行。需将Apollo Client的初始化逻辑移到客户端组件中,确保authMiddleware仅在客户端运行:
拆分authMiddleware为可动态创建的函数
import { signOut, useSession } from "next-auth/react"; import { ApolloLink, Operation, NextLink } from "@apollo/client"; export const createAuthMiddleware = () => { return new ApolloLink((operation: Operation, forward: NextLink) => { const { data: session } = useSession(); let token; if (session) { token = session?.user?.data?.login?.accessToken; } operation.setContext({ headers: { authorization: token ? `Bearer ${token}` : "", }, }); return forward(operation); }); };
修改Apollo Client初始化逻辑为客户端专属
'use client' import { ApolloClient, InMemoryCache, from, HttpLink } from "@apollo/client"; import { onError } from "@apollo/client/link/error"; import { createAuthMiddleware } from "./authMiddleware"; import { CONFLICT_EXCEPTION, INVALID_AUTH_TOKEN, INVALID_OR_EXPIRED_TOKEN_MESSAGE, NOT_FOUND_EXCEPTION, PRECONDITION_FAILED_EXCEPTION, REQUEST_NOT_FOUND, TOKEN_INVALID, TOKEN_NOT_FOUND, UNAUTHENTICATED, UNAUTHORIZED } from "@/app/constants"; import { signOut } from "next-auth/react"; const handleLogout = () => { signOut(); }; const httpLink = new HttpLink({ uri: `${process.env.REACT_APP_API_BASE_URL}/graphql` }); const errorLink = onError(({ graphQLErrors, networkError }: any) => { if (graphQLErrors) { graphQLErrors.map( ({ extensions }: any) => { if (extensions) { const { exception, code } = extensions; if (code === UNAUTHENTICATED) handleLogout(); if (exception) { const { response } = exception as any; if (response) { const { error, response: errorResponse } = response; if (error && error !== REQUEST_NOT_FOUND && error !== NOT_FOUND_EXCEPTION && error !== CONFLICT_EXCEPTION) { if (error === TOKEN_NOT_FOUND) { console.log(INVALID_OR_EXPIRED_TOKEN_MESSAGE); } else { console.log(error); } } if (errorResponse) { const { error: responseError, message } = errorResponse; if (message && message !== REQUEST_NOT_FOUND && message !== NOT_FOUND_EXCEPTION && message !== CONFLICT_EXCEPTION) { console.log(message); } else if (responseError && responseError !== REQUEST_NOT_FOUND && responseError !== NOT_FOUND_EXCEPTION && responseError !== CONFLICT_EXCEPTION && responseError === PRECONDITION_FAILED_EXCEPTION) { console.log(responseError); } } } } } return null; } ); const [{ message }] = graphQLErrors; if (message === UNAUTHORIZED || message === TOKEN_INVALID || message === INVALID_AUTH_TOKEN) { } } if (networkError) console.log(networkError.message); }); export const createApolloClient = () => { return new ApolloClient({ cache: new InMemoryCache(), connectToDevTools: true, link: from([createAuthMiddleware(), errorLink, httpLink]), }); };
更新ApolloProviderComponent使用动态创建的Client
'use client' import { ApolloProvider } from "@apollo/client"; import { createApolloClient } from "../../../apollo"; import { useMemo } from "react"; export default function ApolloProviderComponent({ children, }: Readonly<{ children: React.ReactNode; }>) { const client = useMemo(() => createApolloClient(), []); return ( <ApolloProvider client={client}> <div className="max-[90vw] px-[30px] py-[30px]"> {children} </div> </ApolloProvider> ); }
内容的提问来源于stack exchange,提问作者Saif Haq
相关产品推荐
相关产品推荐

