0Pricing
Next.js 15 Fullstack (App Router + Server Actions) · 课时

用于仪表板与选项卡的条件插槽渲染

将路由片段映射到并行插槽,驱动仪表板区域和选项卡视图。

用于仪表板与选项卡的条件插槽渲染 是 CoddyKit 上的免费 Next.js 15 Fullstack (App Router + Server Actions) 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Next.js 15 Fullstack (App Router + Server Actions) 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Next.js 15 Fullstack (App Router + Server Actions) 课程共包含 4 节课。

本课时的部分内容尚未翻译,以英文显示。

What Are Parallel Routes?

Next.js 15 introduces Parallel Routes, a layout primitive that lets you render multiple pages simultaneously within the same layout — each in its own named slot.

This is ideal for dashboards where you want independent sections like an analytics panel, a team feed, and a notifications sidebar all loading concurrently and independently.

  • Slots are defined with the @slotName folder convention inside a layout segment
  • Each slot is passed as a prop to the parent layout.tsx
  • Slots render in parallel — one can stream while another is already resolved

Think of slots as named holes in your layout that the router fills with different route subtrees simultaneously.

Folder Structure for Parallel Slots

To create parallel slots, place @slotName folders directly inside your route segment folder. Each slot folder acts as its own mini route tree.

A typical dashboard might look like this:

  • app/dashboard/layout.tsx — receives all slot props
  • app/dashboard/@analytics/page.tsx — analytics slot
  • app/dashboard/@team/page.tsx — team feed slot
  • app/dashboard/@notifications/page.tsx — notifications slot

The slot folders do not affect the URL. The URL remains /dashboard while all three slots render simultaneously.

// File: app/dashboard/layout.tsx
// TypeScript interface showing slot props

interface DashboardLayoutProps {
  children: React.ReactNode;
  analytics: React.ReactNode;
  team: React.ReactNode;
  notifications: React.ReactNode;
}

export default function DashboardLayout({
  children,
  analytics,
  team,
  notifications,
}: DashboardLayoutProps) {
  return (
    <div className="dashboard-grid">
      <main className="col-span-2">{children}</main>
      <aside className="analytics-panel">{analytics}</aside>
      <aside className="team-panel">{team}</aside>
      <aside className="notifications-panel">{notifications}</aside>
    </div>
  );
}

The Default File: Handling Unmatched Slots

When you navigate to a URL that does not match a slot's sub-route, Next.js needs to know what to render in that slot. Without a fallback, the entire layout would fail with a 404.

The solution is the default.tsx file inside each slot folder. It acts as a fallback render when the slot has no active match for the current URL.

  • Place a default.tsx in every @slotName folder
  • It can return null to render nothing, or a skeleton/placeholder
  • This keeps other slots functioning even when one has no active sub-route
// File: app/dashboard/@notifications/default.tsx
// Renders nothing when this slot has no active route match

export default function NotificationsDefault() {
  return null;
}

// File: app/dashboard/@analytics/default.tsx
// Could render a skeleton instead
export default function AnalyticsDefault() {
  return (
    <div className="animate-pulse bg-gray-100 rounded-lg h-48" />
  );
}

Conditional Slot Rendering with Server Components

The layout component receives all slots as props — which means you can apply conditional logic directly in the layout to show or hide slots based on server-side data, user roles, or feature flags.

Because layout.tsx is a Server Component by default, you can perform async data fetching right inside it to make this decision.

  • Fetch the current user's role from your database or session
  • Conditionally include or exclude slot props from the rendered output
  • No client-side JavaScript required for the visibility gate
// File: app/dashboard/layout.tsx
import { getCurrentUser } from '@/lib/auth';

interface DashboardLayoutProps {
  children: React.ReactNode;
  analytics: React.ReactNode;
  team: React.ReactNode;
}

export default async function DashboardLayout({
  children,
  analytics,
  team,
}: DashboardLayoutProps) {
  const user = await getCurrentUser();
  const isAdmin = user?.role === 'admin';

  return (
    <div className="dashboard-grid">
      <main>{children}</main>
      {isAdmin && <aside>{analytics}</aside>}
      <aside>{team}</aside>
    </div>
  );
}

Building a Tabbed Dashboard with Parallel Routes

Parallel routes become especially powerful for tabbed interfaces. Instead of using client-side state to track the active tab, you map each tab to a URL segment, making the tab state part of the URL.

This approach gives you:

  • Shareable URLs — users can bookmark a specific tab
  • Browser back/forward navigation between tabs
  • Independent loading states per tab via Suspense
  • Server-rendered tab content with no hydration cost for the tab logic

The tab bar becomes a set of <Link> components pointing to different sub-routes within the slot.

// File: app/dashboard/@tabs/layout.tsx
// Shared tab navigation rendered inside the @tabs slot

import Link from 'next/link';

interface TabsLayoutProps {
  children: React.ReactNode;
}

export default function TabsLayout({ children }: TabsLayoutProps) {
  return (
    <div>
      <nav className="flex gap-2 border-b mb-4">
        <Link
          href="/dashboard/overview"
          className="tab-link"
        >
          Overview
        </Link>
        <Link
          href="/dashboard/revenue"
          className="tab-link"
        >
          Revenue
        </Link>
        <Link
          href="/dashboard/users"
          className="tab-link"
        >
          Users
        </Link>
      </nav>
      <div className="tab-content">{children}</div>
    </div>
  );
}

Mapping Route Segments to Tab Pages

Each tab corresponds to a page.tsx file nested within the @tabs slot. The URL segment becomes the active-tab selector automatically — no JavaScript state needed.

For a dashboard at /dashboard with an @tabs slot:

  • app/dashboard/@tabs/overview/page.tsx — active at /dashboard/overview
  • app/dashboard/@tabs/revenue/page.tsx — active at /dashboard/revenue
  • app/dashboard/@tabs/users/page.tsx — active at /dashboard/users

The parent layout.tsx URL stays the same (/dashboard/*) while only the @tabs slot updates its content.

// File: app/dashboard/@tabs/revenue/page.tsx
import { getRevenueData } from '@/lib/data';

export default async function RevenueTab() {
  const data = await getRevenueData();

  return (
    <section>
      <h2 className="text-xl font-semibold mb-4">Revenue Overview</h2>
      <dl className="grid grid-cols-3 gap-4">
        <div>
          <dt className="text-sm text-gray-500">Monthly Recurring</dt>
          <dd className="text-2xl font-bold">
            ${data.mrr.toLocaleString()}
          </dd>
        </div>
        <div>
          <dt className="text-sm text-gray-500">Annual Run Rate</dt>
          <dd className="text-2xl font-bold">
            ${data.arr.toLocaleString()}
          </dd>
        </div>
        <div>
          <dt className="text-sm text-gray-500">Churn Rate</dt>
          <dd className="text-2xl font-bold">{data.churnRate}%</dd>
        </div>
      </dl>
    </section>
  );
}

Highlighting the Active Tab with usePathname

To visually indicate which tab is active, you need to compare the current URL against each tab's href. The usePathname() hook from next/navigation gives you the current path on the client.

Since the tab bar needs to be interactive (reading the current URL), it must be a Client Component. You can keep only the navigation bar as a Client Component while leaving all tab content as Server Components.

  • Mark the tab navigation file with 'use client'
  • Use usePathname() to detect the active segment
  • Apply conditional classes based on the match
'use client';
// File: app/dashboard/@tabs/_components/TabNav.tsx

import Link from 'next/link';
import { usePathname } from 'next/navigation';

const tabs = [
  { href: '/dashboard/overview', label: 'Overview' },
  { href: '/dashboard/revenue', label: 'Revenue' },
  { href: '/dashboard/users', label: 'Users' },
];

export function TabNav() {
  const pathname = usePathname();

  return (
    <nav className="flex gap-1 border-b">
      {tabs.map((tab) => {
        const isActive = pathname === tab.href;
        return (
          <Link
            key={tab.href}
            href={tab.href}
            className={`px-4 py-2 text-sm font-medium ${
              isActive
                ? 'border-b-2 border-blue-600 text-blue-600'
                : 'text-gray-500 hover:text-gray-700'
            }`}
          >
            {tab.label}
          </Link>
        );
      })}
    </nav>
  );
}

Using Suspense for Independent Slot Loading

One of the biggest advantages of parallel slots is that each slot streams independently. You can wrap individual slots in <Suspense> boundaries so that a slow slot does not block a fast one from rendering.

This eliminates the classic dashboard problem where one slow API call freezes the entire page.

  • Wrap each slot in its own <Suspense> in the layout
  • Each slot shows its own loading skeleton while it resolves
  • Fast slots appear immediately; slow slots stream in when ready
  • No loading.tsx is needed — Suspense gives you fine-grained control
// File: app/dashboard/layout.tsx
import { Suspense } from 'react';
import { AnalyticsSkeleton } from '@/components/skeletons';

interface DashboardLayoutProps {
  children: React.ReactNode;
  analytics: React.ReactNode;
  team: React.ReactNode;
  notifications: React.ReactNode;
}

export default function DashboardLayout({
  children,
  analytics,
  team,
  notifications,
}: DashboardLayoutProps) {
  return (
    <div className="grid grid-cols-3 gap-4">
      <main className="col-span-2">
        <Suspense fallback={<p>Loading main...</p>}>
          {children}
        </Suspense>
      </main>
      <div className="space-y-4">
        <Suspense fallback={<AnalyticsSkeleton />}>
          {analytics}
        </Suspense>
        <Suspense fallback={<p>Loading team...</p>}>
          {team}
        </Suspense>
        <Suspense fallback={null}>
          {notifications}
        </Suspense>
      </div>
    </div>
  );
}

Conditional Slots Based on Search Params

Sometimes you want to control slot visibility with search parameters rather than path segments — for example, toggling a details panel via ?panel=details.

In Server Components, you can access search params via the searchParams prop on page.tsx files, or use useSearchParams() in Client Components.

  • Pass searchParams down from page.tsx to the layout or use a Server Action
  • In the layout, receive search params via the page's data layer rather than directly (layouts do not receive searchParams)
  • A cleaner pattern: use the slot's own page.tsx to read searchParams and conditionally render content
// File: app/dashboard/@details/page.tsx
// Conditionally renders based on ?panel=details

interface DetailsPageProps {
  searchParams: Promise<{ panel?: string }>;
}

export default async function DetailsPanel({
  searchParams,
}: DetailsPageProps) {
  const { panel } = await searchParams;

  if (panel !== 'details') {
    return null;
  }

  return (
    <aside className="border-l pl-4">
      <h3 className="font-semibold">Details Panel</h3>
      <p className="text-sm text-gray-600">
        Expanded details visible when ?panel=details is present.
      </p>
    </aside>
  );
}

Combining Parallel Routes with Server Actions for Tab State

Server Actions pair naturally with parallel-route tabs. You can use a Server Action to mutate data directly from within a tab's page, then rely on Next.js to revalidate and re-render only the affected slot — not the entire page.

This creates a seamless UX: the user submits a form inside a tab, the action runs on the server, the slot re-streams with fresh data, and all other slots remain untouched.

  • Define the Server Action with 'use server' in a separate file or inline
  • Call revalidatePath or revalidateTag to invalidate the data cache
  • Only the relevant slot re-fetches and re-renders
// File: app/dashboard/@tabs/users/actions.ts
'use server';

import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';

export async function deactivateUser(userId: string): Promise<void> {
  await db.user.update({
    where: { id: userId },
    data: { isActive: false },
  });

  // Only the users tab data is invalidated
  revalidatePath('/dashboard/users');
}

// File: app/dashboard/@tabs/users/page.tsx (partial)
// import { deactivateUser } from './actions';
//
// <form action={deactivateUser.bind(null, user.id)}>
//   <button type="submit">Deactivate</button>
// </form>

Soft Navigation and Slot State Preservation

When navigating between tabs or dashboard sections using Next.js <Link>, the router performs a soft navigation. This means:

  • Only the slots whose route segment changed are re-fetched
  • Slots with unchanged routes preserve their existing rendered output and React state
  • Scroll position in unaffected slots is maintained

This is a key advantage over traditional tab implementations using client-side state — you get URL-driven navigation without the cost of re-rendering the entire page tree on every tab switch.

Hard navigation (full page reload) resets all slots, so always use <Link> for tab transitions inside a parallel-route dashboard.

Knowledge Check: Unmatched Slots

You have a dashboard layout with three parallel slots: @analytics, @team, and @notifications. A user navigates to /dashboard/settings, which is defined under children but has no corresponding sub-route in any of the three slot folders.

What happens to the three slots, and what file prevents a 404 error for the whole layout?

Recap: Conditional Slot Rendering for Dashboards and Tabs

In this lesson you learned how to drive dashboard sections and tabbed views using Next.js 15 Parallel Routes.

Key takeaways:

  • Parallel slots use the @slotName folder convention and are passed as props to the parent layout.tsx
  • A default.tsx inside each slot folder is required to handle unmatched URLs and prevent 404 errors
  • The layout is a Server Component, so you can gate slot rendering with async role checks, feature flags, or any server-side data
  • Tab interfaces map each tab to a URL sub-segment within a slot, giving you shareable, bookmarkable, server-rendered tabs with no client state overhead
  • Wrap each slot in its own <Suspense> boundary so slow slots do not block fast ones from streaming
  • Server Actions inside tab pages can call revalidatePath to refresh only the affected slot, leaving all other slots untouched
  • Soft navigation via <Link> re-fetches only changed slots and preserves React state in unchanged ones

Parallel Routes with conditional slot logic give you a composable, URL-driven architecture for complex dashboards — all without a single piece of tab-management client state.

常见问题解答

「用于仪表板与选项卡的条件插槽渲染」课时是免费的吗?

是的 — 「用于仪表板与选项卡的条件插槽渲染」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Next.js 15 Fullstack (App Router + Server Actions) 课程的其余内容,请升级到 CoddyKit PRO。 Next.js 15 Fullstack (App Router + Server Actions) 课程共包含 4 节课。

「用于仪表板与选项卡的条件插槽渲染」这节课中我会学到什么?

将路由片段映射到并行插槽,驱动仪表板区域和选项卡视图。 你通过在浏览器中直接运行的动手代码来练习 Next.js 15 Fullstack (App Router + Server Actions),全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Next.js 15 Fullstack (App Router + Server Actions) 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Next.js 15 Fullstack (App Router + Server Actions) 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。

「用于仪表板与选项卡的条件插槽渲染」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Next.js 15 Fullstack (App Router + Server Actions) 课中编写并运行代码吗?

能。每节 Next.js 15 Fullstack (App Router + Server Actions) 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 带命名插槽与默认片段的并行路由
  2. 用于共享模态体验的拦截路由
  3. 用于仪表板与选项卡的条件插槽渲染
  4. 使用软导航构建照片模态流程
← 返回 Next.js 15 Fullstack (App Router + Server Actions)