Search code examples
app-routernext.js14

Next.js: (App Router): How can I pass props from `page.tsx` to the `Layout.tsx` props?


Context: I have a dashboard which is a route group, and I have a multiple roles, i.e. admin, company and student. I have a sidebar in the Dashboard where the contents may change based on the type of user you are, consider to be RBAC.

Problem: I want to render different sidebar contents based on the route I am visiting.

Project Tree:

.
└── src/
    ├── app/
    │   └── (dashboard)/
    │       ├── admin/
    │       │   └── page.tsx
    │       ├── company/
    │       │   └── page.tsx
    │       ├── student/
    │       │   └── page.tsx
    │       └── layout.tsx
    └── components/
        └── widgets/
            └── dashboard/
                ├── sidebar.tsx
                └── header.tsx

Layout.tsx

import { Sidebar } from "@/components/widgets/dashboard/Sidebar";
import { Header } from "@/components/widgets/dashboard/Header";

interface SidebarItem {
  icon: React.ReactNode;
  title: string;
  href: string;
}

export default async function DashboardLayout({
  children,
  sidebarItems,
}: {
  children: React.ReactNode;
  admin: React.ReactNode;
  sidebarItems: SidebarItem[];
}) {
  return (
    <div className="grid min-h-screen w-full md:grid-cols-[220px_1fr] lg:grid-cols-[280px_1fr]">
      <Sidebar items={sidebarItems} />
      <div className="flex flex-col">
        <Header items={sidebarItems} />
        <main className="flex flex-1 flex-col gap-4 p-4 lg:gap-6 lg:p-6">
          {children}
        </main>
      </div>
    </div>
  );
}

Header.tsx

import Link from "next/link";
import {
  CircleUser,
  Menu,
  Search,
} from "lucide-react";

import { Button } from "@/components/ui/button";
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";

interface SidebarItem {
  icon: React.ReactNode;
  title: string;
  href: string;
}

export function Header({ items = [] }: { items: SidebarItem[] }) {
  return (
    <header className="flex h-14 items-center gap-4 border-b bg-muted/40 px-4 lg:h-[60px] lg:px-6">
      <Sheet>
        <SheetTrigger asChild>
          <Button variant="outline" size="icon" className="shrink-0 md:hidden">
            <Menu className="h-5 w-5" />
            <span className="sr-only">Toggle navigation menu</span>
          </Button>
        </SheetTrigger>
        <SheetContent side="left" className="flex flex-col">
          <nav className="grid gap-2 text-lg font-medium">
            {items.map((item) => (
              <Link
                key={item.title}
                href={item.href}
                className="mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 text-muted-foreground hover:text-foreground"
              >
                {item.icon}
                {item.title}
              </Link>
            ))}
          </nav>
          <div className="mt-auto">
            <Card>
              <CardHeader>
                <CardTitle>Upgrade to Pro</CardTitle>
                <CardDescription>
                  Unlock all features and get unlimited access to our support
                  team.
                </CardDescription>
              </CardHeader>
              <CardContent>
                <Button size="sm" className="w-full">
                  Upgrade
                </Button>
              </CardContent>
            </Card>
          </div>
        </SheetContent>
      </Sheet>
      <div className="w-full flex-1">
        <form>
          <div className="relative">
            <Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
            <Input
              type="search"
              placeholder="Search products..."
              className="w-full appearance-none bg-background pl-8 shadow-none md:w-2/3 lg:w-1/3"
            />
          </div>
        </form>
      </div>
      <DropdownMenu>
        <DropdownMenuTrigger asChild>
          <Button variant="secondary" size="icon" className="rounded-full">
            <CircleUser className="h-5 w-5" />
            <span className="sr-only">Toggle user menu</span>
          </Button>
        </DropdownMenuTrigger>
        <DropdownMenuContent align="end">
          <DropdownMenuLabel>My Account</DropdownMenuLabel>
          <DropdownMenuSeparator />
          <DropdownMenuItem>Settings</DropdownMenuItem>
          <DropdownMenuItem>Support</DropdownMenuItem>
          <DropdownMenuSeparator />
          <DropdownMenuItem>Logout</DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
    </header>
  );
}

Sidebar.tsx

import Link from "next/link";
import { Bell, Package2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";

interface SidebarItem {
  icon: React.ReactNode;
  title: string;
  href: string;
}

export function Sidebar({ items = [] }: { items: SidebarItem[] }) {
  return (
    <div className="hidden border-r bg-muted/40 md:block">
      <div className="flex h-full max-h-screen flex-col gap-2">
        <div className="flex h-14 items-center border-b px-4 lg:h-[60px] lg:px-6">
          <Link href="/" className="flex items-center gap-2 font-semibold">
            <Package2 className="h-6 w-6" />
            <span className="">Acme Inc</span>
          </Link>
          <Button variant="outline" size="icon" className="ml-auto h-8 w-8">
            <Bell className="h-4 w-4" />
            <span className="sr-only">Toggle notifications</span>
          </Button>
        </div>
        <div className="flex-1">
          <nav className="grid items-start px-2 text-sm font-medium lg:px-4">
            {items.map((item) => (
              <Link
                key={item.title}
                href={item.href}
                className="flex items-center gap-3 rounded-lg px-3 py-2 text-muted-foreground transition-all hover:text-primary"
              >
                {item.icon}
                {item.title}
              </Link>
            ))}
          </nav>
        </div>
        <div className="mt-auto p-4">
          <Card>
            <CardHeader className="p-2 pt-0 md:p-4">
              <CardTitle>Upgrade to Pro</CardTitle>
              <CardDescription>
                Unlock all features and get unlimited access to our support
                team.
              </CardDescription>
            </CardHeader>
            <CardContent className="p-2 pt-0 md:p-4 md:pt-0">
              <Button size="sm" className="w-full">
                Upgrade
              </Button>
            </CardContent>
          </Card>
        </div>
      </div>
    </div>
  );
}

admin/page.tsx

import { Button } from "@/components/ui/button";
import {
  Home,
  LineChart,
  Package,
  ShoppingCart,
  Users,
} from "lucide-react";

interface SidebarItem {
  icon: React.ReactNode;
  title: string;
  href: string;
}

const sidebarItems: SidebarItem[] = [
  {
    icon: <Home className="h-4 w-4" />,
    title: "Dashboard",
    href: "#",
  },
  {
    icon: <ShoppingCart className="h-4 w-4" />,
    title: "Orders",
    href: "#",
  },
  {
    icon: <Package className="h-4 w-4" />,
    title: "Products",
    href: "#",
  },
  {
    icon: <Users className="h-4 w-4" />,
    title: "Customers",
    href: "#",
  },
  {
    icon: <LineChart className="h-4 w-4" />,
    title: "Analytics",
    href: "#",
  },
];


export default async function AdminPage() {
  return (
    <>
      <div className="flex items-center">
        <h1 className="text-lg font-semibold md:text-2xl">Inventory</h1>
      </div>
      <div className="flex flex-1 items-center justify-center rounded-lg border border-dashed shadow-sm">
        <div className="flex flex-col items-center gap-1 text-center">
          <h3 className="text-2xl font-bold tracking-tight">
            You have no products
          </h3>
          <p className="text-sm text-muted-foreground">
            You can start selling as soon as you add a product.
          </p>
          <Button className="mt-4">Add Product</Button>
        </div>
      </div>
    </>
  );
}

In this how can I pass the sidebarItems so that it passes in the props defined in the DashboardLayout.tsx ?

Other information:

{
  "name": "website2",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@auth/firebase-adapter": "^1.5.1",
    "@heroicons/react": "^2.1.3",
    "@radix-ui/react-dialog": "^1.0.5",
    "@radix-ui/react-dropdown-menu": "^2.0.6",
    "@radix-ui/react-icons": "^1.3.0",
    "@radix-ui/react-label": "^2.0.2",
    "@radix-ui/react-navigation-menu": "^1.1.4",
    "@radix-ui/react-slot": "^1.0.2",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.1.0",
    "firebase": "^10.9.0",
    "firebase-admin": "^11.11.1",
    "lucide-react": "^0.363.0",
    "next": "14.1.4",
    "next-auth": "^5.0.0-beta.16",
    "react": "^18",
    "react-dom": "^18",
    "react-firebase-hooks": "^5.1.1",
    "tailwind-merge": "^2.2.2",
    "tailwindcss-animate": "^1.0.7"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "autoprefixer": "^10.0.1",
    "eslint": "^8",
    "eslint-config-next": "14.1.4",
    "postcss": "^8",
    "tailwindcss": "^3.3.0",
    "typescript": "^5"
  }
}

Pass props to page.jsx child from root layout (next.js 13)


Solution

  • You'll have to create a separate layout for each role so that you can pass different sidebar items to each layout

    Example: admin/layout.tsx

    export default function Layout({children} : {children: React.ReactNode}) {
       return (<DashboardLayout sidebarItems={adminSidebarItems}>
           {children}
       </DashboardLayout>);
    }
    

    Or you can instead store the role of the signed-in user in a global state. I recommend zustand.