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

Next.js13中NextAuth服务端会话传递至客户端导航栏的问题

Next.js 13 + NextAuth 导航栏服务端会话加载方案

问题背景

在Next.js 13中配置NextAuth后,将layout.tsx设为客户端组件以使用SessionProvider包裹认证组件。页面组件用getServerSession()服务端加载会话正常,但导航栏因嵌套在客户端layout中,尝试用服务端组件加载会话时出现两类错误:

  1. 异步服务端组件无法被客户端组件直接渲染,报错:cannot be used as a JSX component. Its return type 'Promise<Element>' is not a valid JSX element.
  2. 服务端模块bcrypt被客户端webpack解析,出现模块解析错误:Module parse failed: Unexpected token (1:0)

报错详情

组件类型错误

cannot be used as a JSX component.
  Its return type 'Promise<Element>' is not a valid JSX element.

模块解析错误

error - ./node_modules/@mapbox/node-pre-gyp/lib/util/nw-pre-gyp/index.html
Module parse failed: Unexpected token (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file.
> <!doctype html>
| <html>
| <head>

Import trace for requested module:
./node_modules/@mapbox/node-pre-gyp/lib/util/nw-pre-gyp/index.html
./node_modules/@mapbox/node-pre-gyp/lib/ sync ^\.\/.*$
./node_modules/@mapbox/node-pre-gyp/lib/node-pre-gyp.js
./node_modules/bcrypt/bcrypt.js
./pages/api/auth/[...nextauth].ts
./app/(application)/app/AppNav.tsx
./app/(application)/app/layout.tsx

原始代码

layout.tsx

'use client';

import { SessionProvider } from 'next-auth/react';
import '../../globals.css';
import AppNav from './AppNav';

export default function AppLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head />
      <body>
        <SessionProvider>
          <AppNav />
          {children}
        </SessionProvider>
      </body>
    </html>
  );
}

AppNav.tsx

import AppMainNavigation from '@/components/app/universal/navigation/AppMainNavigation';
import { authOptions } from '@/pages/api/auth/[...nextauth]';
import { getServerSession } from 'next-auth';

export default async function AppNav() {
  const session = await getServerSession(authOptions);

  console.log(`App navigation: ${session}`);
  return <AppMainNavigation />;
}

AppMainNavigation.tsx

'use client';

import React, { useState } from 'react';
import { signOut } from 'next-auth/react';

const AppMainNavigation = () => {
const [MobileMenuState, setMobileMenuState] = useState(false);

return (
  <div>
      <a
        href="#"
        className=""
        onClick={() => signOut({ callbackUrl: '/login' })}
      >
        SignOut
      </a>
      <button
        className="md:hidden rounded-lg focus:outline-none focus:shadow-outline"
        onClick={() => {
          console.log(
            `Mobile Menu Button Clicked. Current State ${!MobileMenuState}`
          );
          setMobileMenuState(!MobileMenuState);
        }}
      >
        <svg fill="currentColor" viewBox="0 0 20 20" className="w-6 h-6">
          <path
            className={`${MobileMenuState ? 'hidden' : 'block'}`}
            fillRule="evenodd"
            d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM9 15a1 1 0 011-1h6a1 1 0 110 2h-6a1 1 0 01-1-1z"
            clipRule="evenodd"
          ></path>
          <path
            className={`${MobileMenuState ? 'block' : 'hidden'}`}
            fillRule="evenodd"
            d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
            clipRule="evenodd"
          ></path>
        </svg>
      </button>
  </div>
);
};

export default AppMainNavigation;

解决方案

1. 拆分导航组件:服务端数据层 + 客户端交互层

将导航的会话获取逻辑(服务端)和交互逻辑(客户端)分离,避免客户端直接渲染异步组件。

创建服务端组件 AppNavServer.tsx

负责在服务端获取会话,并传递给客户端组件:

import { authOptions } from '@/lib/auth';
import { getServerSession } from 'next-auth';
import AppNavClient from './AppNavClient';

export default async function AppNavServer() {
  const session = await getServerSession(authOptions);
  return <AppNavClient session={session} />;
}

创建客户端组件 AppNavClient.tsx

负责渲染交互逻辑,接收服务端传递的会话:

'use client';

import AppMainNavigation from '@/components/app/universal/navigation/AppMainNavigation';

type Props = {
  session: any;
};

export default function AppNavClient({ session }: Props) {
  return <AppMainNavigation session={session} />;
}

2. 调整Layout结构,分离服务端与客户端逻辑

将根Layout改为服务端组件,嵌套客户端组件提供SessionProvider:

修改 layout.tsx(服务端组件,移除'use client')

import '../../globals.css';
import AppNavServer from './AppNavServer';
import ClientLayoutWrapper from './ClientLayoutWrapper';

export default function AppLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head />
      <body>
        <AppNavServer />
        <ClientLayoutWrapper>{children}</ClientLayoutWrapper>
      </body>
    </html>
  );
}

创建客户端组件 ClientLayoutWrapper.tsx

专门提供SessionProvider包裹页面内容:

'use client';

import { SessionProvider } from 'next-auth/react';

export default function ClientLayoutWrapper({ children }: { children: React.ReactNode }) {
  return <SessionProvider>{children}</SessionProvider>;
}

3. 修复bcrypt模块错误

将authOptions移至服务端专用文件(如lib/auth.ts),避免客户端间接引用服务端模块:

创建 lib/auth.ts

import type { AuthOptions } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import bcrypt from 'bcrypt';

export const authOptions: AuthOptions = {
  providers: [
    CredentialsProvider({
      name: 'Credentials',
      credentials: {
        email: { label: 'Email', type: 'email' },
        password: { label: 'Password', type: 'password' }
      },
      async authorize(credentials) {
        if (!credentials?.email || !credentials?.password) return null;
        
        // 示例:从数据库获取用户并验证密码
        const user = { id: '1', email: 'user@example.com', passwordHash: '...' };
        const isPasswordValid = await bcrypt.compare(credentials.password, user.passwordHash);
        
        return isPasswordValid ? user : null;
      }
    })
  ],
  session: { strategy: 'jwt' }
};

4. 更新导航交互组件

根据传入的会话状态调整渲染逻辑:

'use client';

import React, { useState } from 'react';
import { signOut } from 'next-auth/react';

type Props = {
  session: any;
};

const AppMainNavigation = ({ session }: Props) => {
  const [mobileMenuState, setMobileMenuState] = useState(false);

  return (
    <div>
      {session ? (
        <a
          href="#"
          className=""
          onClick={(e) => {
            e.preventDefault();
            signOut({ callbackUrl: '/login' });
          }}
        >
          Sign Out
        </a>
      ) : (
        <a href="/login" className="">Sign In</a>
      )}
      <button
        className="md:hidden rounded-lg focus:outline-none focus:shadow-outline"
        onClick={() => setMobileMenuState(!mobileMenuState)}
      >
        <svg fill="currentColor" viewBox="0 0 20 20" className="w-6 h-6">
          <path
            className={mobileMenuState ? 'hidden' : 'block'}
            fillRule="evenodd"
            d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM9 15a1 1 0 011-1h6a1 1 0 110 2h-6a1 1 0 01-1-1z"
            clipRule="evenodd"
          ></path>
          <path
            className={mobileMenuState ? 'block' : 'hidden'}
            fillRule="evenodd"
            d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
            clipRule="evenodd"
          ></path>
        </svg>
      </button>
    </div>
  );
};

export default AppMainNavigation;

核心逻辑说明

  • 服务端组件负责会话获取,确保导航栏与页面会话状态一致,消除加载延迟。
  • 客户端组件仅处理交互逻辑,通过props接收服务端数据,避免直接调用服务端API。
  • 分离服务端与客户端代码,防止服务端专属模块(如bcrypt)被客户端webpack解析,解决模块错误。

内容的提问来源于stack exchange,提问作者cphill

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 10:32:18