לדלג לתוכן

9.2 ראוטר, דפים ולייאאוטים הרצאה

ראוטר, דפים ולייאאוטים - Router, Pages, and Layouts

בשיעור זה נלמד לעומק את מערכת הראוטינג של Next.js App Router. נבין איך קבצים ותיקיות הופכים לנתיבים, איך עובדים עם לייאאוטים, ואיך לנווט באפליקציה.


ראוטינג מבוסס קבצים - File-Based Routing

  • בנקסט, הראוטינג נקבע אוטומטית לפי מבנה התיקיות בתיקיית app
  • כל תיקייה מייצגת חלק מהנתיב (segment)
  • קובץ page.tsx בתוך תיקייה הופך אותה לנתיב נגיש
app/
  page.tsx              →  /
  about/
    page.tsx            →  /about
  blog/
    page.tsx            →  /blog
    post/
      page.tsx          →  /blog/post
  • תיקייה בלי page.tsx לא תהיה נגישה כנתיב
  • זה שונה מ-Pages Router הישן שבו כל קובץ היה נתיב

קבצים מיוחדים - Special Files

נקסט מכיר כמה קבצים מיוחדים שכל אחד ממלא תפקיד שונה:

דף - page.tsx

  • מגדיר את התוכן הייחודי של הנתיב
  • חייב לייצא קומפוננטה כ-default export
  • בלי page.tsx, הנתיב לא נגיש
// app/about/page.tsx
export default function AboutPage() {
  return (
    <div>
      <h1>אודות</h1>
      <p>דף האודות שלנו</p>
    </div>
  );
}

לייאאוט - layout.tsx

  • עוטף את הדף ואת כל הלייאאוטים המקוננים
  • לא מתרנדר מחדש בניווט - שומר על state
  • הלייאאוט הראשי (app/layout.tsx) הוא חובה
// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="he" dir="rtl">
      <body>
        <header>כותרת</header>
        {children}
        <footer>תחתית</footer>
      </body>
    </html>
  );
}

טעינה - loading.tsx

  • מוצג אוטומטית בזמן שהדף נטען
  • עוטף את ה-page ב-React Suspense מאחורי הקלעים
  • מאפשר חווית משתמש טובה יותר
// app/blog/loading.tsx
export default function Loading() {
  return (
    <div className="flex items-center justify-center p-8">
      <div className="animate-spin h-8 w-8 border-4 border-blue-500 border-t-transparent rounded-full" />
      <span className="mr-3">טוען...</span>
    </div>
  );
}

שגיאה - error.tsx

  • מוצג כשיש שגיאה ברינדור של הדף
  • חייב להיות Client Component ("use client")
  • מקבל את השגיאה ופונקציה לאיפוס
// app/blog/error.tsx
"use client";

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div className="p-8 text-center">
      <h2 className="text-2xl font-bold text-red-600">משהו השתבש</h2>
      <p className="mt-2">{error.message}</p>
      <button
        onClick={reset}
        className="mt-4 px-4 py-2 bg-blue-500 text-white rounded"
      >
        נסה שוב
      </button>
    </div>
  );
}

לא נמצא - not-found.tsx

  • מוצג כשקוראים ל-notFound() או כשנתיב לא קיים
  • ברמה הראשית (app/not-found.tsx) תופס את כל הנתיבים שלא קיימים
// app/not-found.tsx
import Link from "next/link";

export default function NotFound() {
  return (
    <div className="p-8 text-center">
      <h2 className="text-3xl font-bold">404 - הדף לא נמצא</h2>
      <p className="mt-2">הדף שחיפשת לא קיים</p>
      <Link href="/" className="mt-4 inline-block text-blue-500 underline">
        חזרה לדף הבית
      </Link>
    </div>
  );
}

סדר הירארכיית הקבצים

layout.tsx
  loading.tsx
    error.tsx
      page.tsx
  • הלייאאוט עוטף הכל
  • בתוכו ה-loading (Suspense boundary)
  • בתוכו ה-error (Error boundary)
  • ובפנים ה-page עצמו

לייאאוטים מקוננים - Nested Layouts

אחד הפיצ'רים החזקים ביותר של App Router הוא לייאאוטים מקוננים:

app/
  layout.tsx              # לייאאוט ראשי - navbar + footer
  page.tsx                # דף הבית
  dashboard/
    layout.tsx            # לייאאוט דשבורד - sidebar
    page.tsx              # /dashboard
    settings/
      page.tsx            # /dashboard/settings
    analytics/
      page.tsx            # /dashboard/analytics
// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex">
      <aside className="w-64 bg-gray-100 p-4 min-h-screen">
        <nav>
          <ul className="space-y-2">
            <li>
              <a href="/dashboard">סקירה כללית</a>
            </li>
            <li>
              <a href="/dashboard/settings">הגדרות</a>
            </li>
            <li>
              <a href="/dashboard/analytics">אנליטיקס</a>
            </li>
          </ul>
        </nav>
      </aside>
      <main className="flex-1 p-6">{children}</main>
    </div>
  );
}
  • כשעוברים בין /dashboard/settings ל-/dashboard/analytics, הלייאאוט של הדשבורד לא מתרנדר מחדש
  • רק ה-children (התוכן של הדף) משתנה
  • זה שומר על ה-state של הסיידבר ומונע טעינות מיותרות

קבוצות נתיבים - Route Groups

לפעמים רוצים לארגן קבצים בתיקיות בלי שזה ישפיע על הנתיב. משתמשים בסוגריים:

app/
  (marketing)/
    about/
      page.tsx          →  /about
    pricing/
      page.tsx          →  /pricing
    layout.tsx          # לייאאוט ייחודי לדפי שיווק
  (app)/
    dashboard/
      page.tsx          →  /dashboard
    settings/
      page.tsx          →  /settings
    layout.tsx          # לייאאוט ייחודי לאפליקציה
  layout.tsx            # לייאאוט ראשי
  • תיקייה עם סוגריים (name) לא מופיעה בנתיב
  • מאפשר לייאאוטים שונים לקבוצות שונות של דפים
  • שימושי לארגון קוד ולהפרדה בין חלקי האתר
// app/(marketing)/layout.tsx
export default function MarketingLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div>
      <nav className="bg-white shadow p-4">
        <h1>האתר שלנו</h1>
      </nav>
      {children}
    </div>
  );
}

// app/(app)/layout.tsx
export default function AppLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex">
      <aside className="w-64 bg-gray-900 text-white p-4">
        <h1>דשבורד</h1>
      </aside>
      <main className="flex-1">{children}</main>
    </div>
  );
}

ניווט - Navigation

נקסט מספק קומפוננטת Link לניווט בין דפים:

import Link from "next/link";

export default function Navbar() {
  return (
    <nav className="flex gap-4 p-4">
      <Link href="/">בית</Link>
      <Link href="/about">אודות</Link>
      <Link href="/blog">בלוג</Link>
      <Link href="/blog/my-post">פוסט ספציפי</Link>
    </nav>
  );
}
  • Link עושה prefetch אוטומטי - טוען את הדף הבא מראש
  • ניווט מהיר בלי טעינה מחדש של כל הדף (client-side navigation)
  • תומך ב-replace כדי להחליף את הנתיב ב-history במקום להוסיף
// החלפה ב-history במקום הוספה
<Link href="/login" replace>
  התחבר
</Link>

// גלילה לאלמנט ספציפי
<Link href="/blog#comments">
  לתגובות
</Link>

// ביטול prefetch
<Link href="/heavy-page" prefetch={false}>
  דף כבד
</Link>

הוק useRouter

לניווט פרוגרמטי (מקוד, לא מלחיצה) משתמשים ב-useRouter:

"use client";

import { useRouter } from "next/navigation";

export default function LoginForm() {
  const router = useRouter();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    // לוגיקת התחברות...

    router.push("/dashboard"); // ניווט לדשבורד
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" placeholder="אימייל" />
      <input type="password" placeholder="סיסמה" />
      <button type="submit">התחבר</button>
    </form>
  );
}
  • router.push("/path") - ניווט לנתיב חדש
  • router.replace("/path") - החלפת הנתיב הנוכחי (לא נוסף ל-history)
  • router.back() - חזרה אחורה
  • router.forward() - קדימה
  • router.refresh() - ריענון הנתונים בלי טעינה מלאה

הוק usePathname

מחזיר את הנתיב הנוכחי:

"use client";

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

export default function Navbar() {
  const pathname = usePathname();

  const links = [
    { href: "/", label: "בית" },
    { href: "/about", label: "אודות" },
    { href: "/blog", label: "בלוג" },
  ];

  return (
    <nav className="flex gap-4 p-4">
      {links.map((link) => (
        <Link
          key={link.href}
          href={link.href}
          className={pathname === link.href ? "font-bold text-blue-600" : ""}
        >
          {link.label}
        </Link>
      ))}
    </nav>
  );
}
  • שימושי לסימון הלינק הפעיל בתפריט
  • חייב להיות בתוך Client Component ("use client")

הוק useSearchParams

מאפשר לקרוא פרמטרים מה-URL:

"use client";

import { useSearchParams } from "next/navigation";

export default function SearchPage() {
  const searchParams = useSearchParams();
  const query = searchParams.get("q");
  const page = searchParams.get("page") || "1";

  return (
    <div>
      <h1>חיפוש</h1>
      {query ? (
        <p>
          מציג תוצאות עבור: {query} (עמוד {page})
        </p>
      ) : (
        <p>הכנס מילת חיפוש</p>
      )}
    </div>
  );
}

// URL: /search?q=nextjs&page=2
// query = "nextjs", page = "2"

דוגמה מלאה - אתר עם ניווט מלא

app/
  layout.tsx
  page.tsx
  (marketing)/
    about/
      page.tsx
    contact/
      page.tsx
  (app)/
    layout.tsx
    dashboard/
      page.tsx
      loading.tsx
      error.tsx
    profile/
      page.tsx
  not-found.tsx

לייאאוט ראשי:

// app/layout.tsx
import type { Metadata } from "next";
import "./globals.css";
import Navbar from "./components/Navbar";

export const metadata: Metadata = {
  title: "האתר שלי",
  description: "אתר לדוגמה עם Next.js",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="he" dir="rtl">
      <body className="min-h-screen">
        <Navbar />
        {children}
      </body>
    </html>
  );
}

קומפוננטת ניווט:

// app/components/Navbar.tsx
"use client";

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

const links = [
  { href: "/", label: "בית" },
  { href: "/about", label: "אודות" },
  { href: "/contact", label: "צור קשר" },
  { href: "/dashboard", label: "דשבורד" },
];

export default function Navbar() {
  const pathname = usePathname();

  return (
    <nav className="bg-gray-800 text-white p-4">
      <div className="max-w-6xl mx-auto flex gap-6">
        {links.map((link) => (
          <Link
            key={link.href}
            href={link.href}
            className={
              pathname === link.href
                ? "text-yellow-400 font-bold"
                : "hover:text-gray-300"
            }
          >
            {link.label}
          </Link>
        ))}
      </div>
    </nav>
  );
}

לייאאוט אפליקציה:

// app/(app)/layout.tsx
import Link from "next/link";

export default function AppLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex">
      <aside className="w-64 bg-gray-100 p-4 min-h-screen">
        <h2 className="font-bold mb-4">תפריט</h2>
        <nav className="space-y-2">
          <Link href="/dashboard" className="block hover:text-blue-600">
            סקירה כללית
          </Link>
          <Link href="/profile" className="block hover:text-blue-600">
            פרופיל
          </Link>
        </nav>
      </aside>
      <main className="flex-1 p-6">{children}</main>
    </div>
  );
}

טמפלייט - template.tsx

  • דומה ל-layout.tsx אבל יוצר instance חדש בכל ניווט
  • לא שומר על state - מתרנדר מחדש כל פעם
  • שימושי לאנימציות כניסה/יציאה
// app/blog/template.tsx
export default function BlogTemplate({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="animate-fadeIn">
      {children}
    </div>
  );
}

סיכום

  • ראוטינג בנקסט מבוסס על מבנה תיקיות בתיקיית app
  • קבצים מיוחדים: page.tsx, layout.tsx, loading.tsx, error.tsx, not-found.tsx
  • לייאאוטים מקוננים שומרים state ולא מתרנדרים מחדש בניווט
  • קבוצות נתיבים (name) מאפשרות ארגון בלי השפעה על הנתיב
  • קומפוננטת Link לניווט מהיר עם prefetch
  • הוקים: useRouter לניווט פרוגרמטי, usePathname לנתיב נוכחי, useSearchParams לפרמטרים