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

Next.js 13客户端Navbar:用户查询与菜单状态管理问题

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">&rarr;</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">&rarr;</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组件需要用户数据,记得给它传递user props,确保能正确渲染用户信息。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 23:34:55