Next.js13中NextAuth服务端会话传递至客户端导航栏的问题
Next.js 13 + NextAuth 导航栏服务端会话加载方案
问题背景
在Next.js 13中配置NextAuth后,将layout.tsx设为客户端组件以使用SessionProvider包裹认证组件。页面组件用getServerSession()服务端加载会话正常,但导航栏因嵌套在客户端layout中,尝试用服务端组件加载会话时出现两类错误:
- 异步服务端组件无法被客户端组件直接渲染,报错:
cannot be used as a JSX component. Its return type 'Promise<Element>' is not a valid JSX element. - 服务端模块
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
相关产品推荐
相关产品推荐

