Next.js 13客户端Navbar:用户查询与菜单状态管理问题
你的核心矛盾是客户端组件无法直接执行服务器端异步操作(比如调用getCurrentUser),而服务器组件又不能用useState管理UI状态,解决思路是把Navbar拆分为「服务器组件(负责数据获取)」和「客户端组件(负责交互状态)」两部分,通过props传递数据。
具体重构步骤
1. 创建服务器组件:负责获取用户数据
新建NavbarServer.tsx(不要加'use client'指令),在这里调用getCurrentUser获取用户信息,再渲染客户端Navbar组件并传递数据:
// NavbarServer.tsx import { User } from '@prisma/client'; import NavbarClient from './NavbarClient'; import getCurrentUser from '@/app/actions/getCurrentUser'; export default async function NavbarServer() { const currentUser = await getCurrentUser(); return <NavbarClient currentUser={currentUser} />; }
2. 修改原Navbar为客户端组件:负责UI与状态管理
把原Navbar.tsx重命名为NavbarClient.tsx,移除async关键字,通过props接收currentUser,专注处理移动端菜单的状态逻辑:
// NavbarClient.tsx 'use client' import { Dialog } from '@headlessui/react' import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline' import { User } from '@prisma/client' import Link from 'next/link' import { useState } from 'react' import { UserAvatar } from './navbar/UserAvatar' interface NavbarClientProps { currentUser: User | null; } export default function NavbarClient({ currentUser }: NavbarClientProps) { const [mobileMenuOpen, setMobileMenuOpen] = useState(false); return ( <header className="bg-slate-50"> <nav className="mx-auto flex max-w-7xl items-center justify-between p-6 lg:px-8" aria-label="Global"> {/* logo */} <div className="flex"> <a href="#" className="-m-1.5 p-1.5"> <span className="sr-only">Your Company</span> <img className="h-8 w-auto" src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600" alt="" /> </a> </div> {/* mobile menu trigger */} <div className="flex lg:hidden"> <button type="button" className="-m-2.5 inline-flex items-center justify-center rounded-md p-2.5 text-gray-700" onClick={() => setMobileMenuOpen(true)}> <span className="sr-only">Open main menu</span> <Bars3Icon className="h-6 w-6" aria-hidden="true" /> </button> </div> {/* desktop menu items */} <div className="hidden lg:flex lg:gap-x-12"> <a href="#" className="text-sm font-semibold leading-6 text-gray-900"> Features </a> <a href="#" className="text-sm font-semibold leading-6 text-gray-900"> Marketplace </a> <a href="#" className="text-sm font-semibold leading-6 text-gray-900"> Company </a> </div> {/* desktop auth section */} <div className='hidden lg:flex'> {currentUser ? ( <UserAvatar user={currentUser} /> ) : ( <Link href='/sign-in' className="hidden lg:flex lg:flex-1 lg:justify-end"> Sign In <span aria-hidden="true">→</span> </Link> )} </div> </nav> {/* mobile menu dialog */} <Dialog as="div" className="lg:hidden" open={mobileMenuOpen} onClose={setMobileMenuOpen}> <div className="fixed inset-0 z-10" /> <Dialog.Panel className="fixed inset-y-0 right-0 z-10 w-full overflow-y-auto bg-white px-6 py-6 sm:max-w-sm sm:ring-1 sm:ring-gray-900/10"> <div className="flex items-center justify-between"> <a href="#" className="-m-1.5 p-1.5"> <span className="sr-only">Your Company</span> <img className="h-8 w-auto" src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600" alt="" /> </a> <button type="button" className="-m-2.5 rounded-md p-2.5 text-gray-700" onClick={() => setMobileMenuOpen(false)}> <span className="sr-only">Close menu</span> <XMarkIcon className="h-6 w-6" aria-hidden="true" /> </button> </div> <div className="mt-6 flow-root"> <div className="-my-6 divide-y divide-gray-500/10"> <div className="space-y-2 py-6"> <a href="#" className="-mx-3 block rounded-lg px-3 py-2 text-base font-semibold leading-7 text-gray-900 hover:bg-gray-50"> Features </a> <a href="#" className="-mx-3 block rounded-lg px-3 py-2 text-base font-semibold leading-7 text-gray-900 hover:bg-gray-50"> Marketplace </a> <a href="#" className="-mx-3 block rounded-lg px-3 py-2 text-base font-semibold leading-7 text-gray-900 hover:bg-gray-50"> Company </a> </div> <div className="py-6"> {currentUser ? ( <UserAvatar user={currentUser} /> ) : ( <Link href="/sign-in" className="-mx-3 block rounded-lg px-3 py-2.5 text-base font-semibold leading-7 text-gray-900 hover:bg-gray-50" > Sign in <span aria-hidden="true">→</span> </Link> )} </div> </div> </div> </Dialog.Panel> </Dialog> </header> ) }
3. 调整页面导入
在使用Navbar的页面(比如app/layout.tsx),导入NavbarServer代替原来的Navbar:
// app/layout.tsx import NavbarServer from './components/NavbarServer'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <NavbarServer /> {children} </body> </html> ); }
关键说明
- 服务器组件(
NavbarServer)运行在服务器端,可安全调用getServerSession和数据库查询,避免客户端暴露敏感逻辑。 - 客户端组件(
NavbarClient)只负责UI渲染和交互状态(比如mobileMenuOpen),通过props接收服务器传递的用户数据,完全符合Next.js App Router的组件模型。 - 如果
UserAvatar组件需要用户数据,记得给它传递userprops,确保能正确渲染用户信息。
内容的提问来源于stack exchange,提问作者Tomas Lukac
相关产品推荐
相关产品推荐

