9.2 ראוטר, דפים ולייאאוטים הרצאה
ראוטר, דפים ולייאאוטים - Router, Pages, and Layouts¶
בשיעור זה נלמד לעומק את מערכת הראוטינג של Next.js App Router. נבין איך קבצים ותיקיות הופכים לנתיבים, איך עובדים עם לייאאוטים, ואיך לנווט באפליקציה.
ראוטינג מבוסס קבצים - File-Based Routing¶
- בנקסט, הראוטינג נקבע אוטומטית לפי מבנה התיקיות בתיקיית
app - כל תיקייה מייצגת חלק מהנתיב (segment)
- קובץ
page.tsxבתוך תיקייה הופך אותה לנתיב נגיש
- תיקייה בלי
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>
);
}
סדר הירארכיית הקבצים¶
- הלייאאוט עוטף הכל
- בתוכו ה-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 Component¶
נקסט מספק קומפוננטת 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לפרמטרים