You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 20:10:53