全栈开发

nextjs导航栏用户信息管理

主要是为了解决登录用户信息页面显示的时候会先加载Loading然后显示用户信息问题

1、server端获取用户信息

"use server";
import { fetchWithAuth } from "@/lib/server-fetch";

export async function fetchCurrentUser() {
    try {
        const res = await fetchWithAuth("/user/profile", { method: "GET" });
        const { data } = await res.json();
        return data ?? null; // 直接返回 UserInfo
    } catch (err) {
        console.error("fetchCurrentUser error:", err);
        return null;
    }
}

2、layout server端渲染页面前加载用户信息

import { ReactNode } from "react";
import {
  SidebarInset,
  SidebarProvider,
  SidebarTrigger,
} from "@/components/ui/sidebar";
import { fetchCurrentUser } from "@/lib/user";
import { AppSidebar } from "@/components/app-sidebar";
import { NavUser } from "@/components/nav-user";
import { ModeToggle } from "@/components/mode-toggle";
import { Separator } from "@/components/ui/separator";

export default async function DashboardLayout({ children }: { children: ReactNode }) {
  // 服务端获取当前登录用户信息
  const user = await fetchCurrentUser();

  return (
    <SidebarProvider>
      <AppSidebar />
      <SidebarInset>
        <header className="flex h-16 shrink-0 items-center gap-2">
          <div className="flex items-center gap-2 px-4">
            <SidebarTrigger className="-ml-1" />
            <Separator
              orientation="vertical"
              className="mr-2 data-[orientation=vertical]:h-4"
            />
          </div>
          <div className="ml-auto px-4">
            <div className="flex items-center gap-2 text-sm">
              <ModeToggle />
              {user && <NavUser initialUser={user} />}
            </div>
          </div>
        </header>
        <main className="flex flex-1 flex-col gap-4 p-4 pt-0">
          {children}
        </main>
      </SidebarInset>
    </SidebarProvider>
  );
}

3、导航用户组件 接收server端用户信息

"use client"

import {
  BadgeCheck,
  Bell,
  ChevronsUpDown,
  CreditCard,
  LogOut,
} from "lucide-react"

import {
  Avatar,
  AvatarFallback,
  AvatarImage,
} from "@/components/ui/avatar"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
} from "@/components/ui/sidebar"
import { UserInfo } from "@/app/(public)/login/_libs/use-auth-store"
import { useRouter } from "next/navigation"
import { logout } from "@/app/(public)/login/_services/auth"
import { alert } from "@/lib/use-global-store"
import Link from "next/link"
import { useCurrentUser } from "@/app/(protected)/dashboard/settings/user/services/use-user-queries"

export function NavUser({ initialUser }: { initialUser?: UserInfo }) {
  const { data: currentUser } = useCurrentUser();
  const user = currentUser || initialUser;
  const router = useRouter();

  const handleLogout = async () => {
    await logout();
    router.replace("/login");
  }

  if (!user) {
    return null;
  }

  return (
    <SidebarMenu>
      <SidebarMenuItem>
        <DropdownMenu>
          <DropdownMenuTrigger asChild>
            <SidebarMenuButton
              size="lg"
              className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
            >
              <Avatar className="h-8 w-8 rounded-lg">
                <AvatarImage src={user.avatar} alt={user.fullName} />
                <AvatarFallback className="rounded-lg">CN</AvatarFallback>
              </Avatar>
              <div className="grid flex-1 text-left text-sm leading-tight">
                <span className="truncate font-medium">{user.fullName}</span>
                <span className="truncate text-xs">{user.email}</span>
              </div>
              <ChevronsUpDown className="ml-auto size-4" />
            </SidebarMenuButton>
          </DropdownMenuTrigger>
          <DropdownMenuContent
            className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
            side="bottom"
            align="end"
            sideOffset={4}
          >
            <DropdownMenuGroup>
              <DropdownMenuItem asChild>
                <Link href="/dashboard/settings/user">
                  <BadgeCheck />
                  个人中心
                </Link>
              </DropdownMenuItem>
              <DropdownMenuItem>
                <CreditCard />
                Billing
              </DropdownMenuItem>
              <DropdownMenuItem>
                <Bell />
                Notifications
              </DropdownMenuItem>
            </DropdownMenuGroup>
            <DropdownMenuSeparator />
            <DropdownMenuItem variant="destructive" onClick={() => alert({
              onConfirm: handleLogout,
              title: "警告!",
              description: "确定退出?",
              confirmLabel: "确定",
              cancelLabel: "取消"
            })}>
              <LogOut />
              退出
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      </SidebarMenuItem>
    </SidebarMenu>
  )
}

这里的useCurrentUser来自React Query 缓存,支持不刷页面更新数据

import { fetchCurrentUser } from "@/lib/user";
import { useQuery } from "@tanstack/react-query";

export const useCurrentUser = () =>
  useQuery({
    queryKey: ["currentUser"],
    queryFn: fetchCurrentUser,
    staleTime: 1000 * 60, // 缓存 1 分钟
  });

说明:

  • const user = currentUser || initialUser; 在获取缓存时currentUser无值使用server端user传值
  • 也就是在空窗期有值可以直接渲染到页面,不使用Loading效果