7.4 ריאקט ראוטר פתרון
פתרון - ריאקט ראוטר - React Router¶
פתרון תרגיל 1 - אתר תדמית בסיסי¶
import {
BrowserRouter,
Routes,
Route,
NavLink,
useNavigate,
} from "react-router-dom";
function App() {
return (
<BrowserRouter>
<Navigation />
<main style={{ padding: "20px" }}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/services" element={<Services />} />
<Route path="/contact" element={<Contact />} />
<Route path="*" element={<NotFound />} />
</Routes>
</main>
</BrowserRouter>
);
}
function Navigation() {
const linkStyle = ({ isActive }: { isActive: boolean }) => ({
padding: "10px 20px",
textDecoration: "none",
color: isActive ? "white" : "#333",
backgroundColor: isActive ? "#3b82f6" : "transparent",
borderRadius: "4px",
});
return (
<nav
style={{
display: "flex",
gap: "8px",
padding: "16px",
backgroundColor: "#f5f5f5",
}}
>
<NavLink to="/" style={linkStyle} end>בית</NavLink>
<NavLink to="/about" style={linkStyle}>אודות</NavLink>
<NavLink to="/services" style={linkStyle}>שירותים</NavLink>
<NavLink to="/contact" style={linkStyle}>צרו קשר</NavLink>
</nav>
);
}
function Home() {
return (
<div>
<h1>ברוכים הבאים</h1>
<p>אתר התדמית שלנו</p>
</div>
);
}
function About() {
return (
<div>
<h1>אודותינו</h1>
<p>אנחנו חברת פיתוח תוכנה מובילה</p>
</div>
);
}
function Services() {
return (
<div>
<h1>השירותים שלנו</h1>
<ul>
<li>פיתוח אתרים</li>
<li>פיתוח אפליקציות</li>
<li>עיצוב UI/UX</li>
</ul>
</div>
);
}
function Contact() {
return (
<div>
<h1>צרו קשר</h1>
<p>אימייל: info@example.com</p>
<p>טלפון: 050-1234567</p>
</div>
);
}
function NotFound() {
const navigate = useNavigate();
return (
<div style={{ textAlign: "center" }}>
<h1>404 - הדף לא נמצא</h1>
<p>הדף שחיפשת לא קיים</p>
<button onClick={() => navigate("/")}>חזרה לדף הבית</button>
</div>
);
}
הסבר:
- הפרופ end ב-NavLink של דף הבית מונע מהלינק להיות פעיל בכל נתיב (כי "/" מתאים לכל נתיב)
- הפונקציה linkStyle מקבלת אובייקט עם isActive ומחזירה סגנון מתאים
- דף 404 תופס כל נתיב שלא הוגדר עם path="*"
פתרון תרגיל 2 - בלוג עם נתיבים דינמיים¶
import {
BrowserRouter,
Routes,
Route,
Link,
useParams,
useNavigate,
} from "react-router-dom";
interface Post {
id: number;
title: string;
date: string;
excerpt: string;
content: string;
}
const posts: Post[] = [
{
id: 1,
title: "מבוא לריאקט",
date: "2024-01-15",
excerpt: "למדו את הבסיס של ריאקט",
content: "ריאקט היא ספריית JavaScript לבניית ממשקי משתמש. היא פותחה על ידי פייסבוק ומאפשרת לבנות קומפוננטות UI ניתנות לשימוש חוזר.",
},
{
id: 2,
title: "הוקים מתקדמים",
date: "2024-02-20",
excerpt: "הוקים מתקדמים בריאקט",
content: "הוקים מתקדמים כמו useMemo, useCallback ו-useRef מאפשרים אופטימיזציה של ביצועים ושליטה דקה יותר בקומפוננטות.",
},
{
id: 3,
title: "ניהול סטייט",
date: "2024-03-10",
excerpt: "איך לנהל state באפליקציות ריאקט",
content: "ניהול state הוא אחד האתגרים המרכזיים בפיתוח אפליקציות ריאקט. ישנן מספר גישות: useState, useReducer, Context, וספריות חיצוניות.",
},
];
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<PostList />} />
<Route path="/posts/:postId" element={<PostDetail />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}
function PostList() {
return (
<div>
<h1>הבלוג שלנו</h1>
{posts.map((post) => (
<article
key={post.id}
style={{
border: "1px solid #ddd",
padding: "16px",
margin: "12px 0",
borderRadius: "8px",
}}
>
<Link to={`/posts/${post.id}`} style={{ textDecoration: "none" }}>
<h2>{post.title}</h2>
</Link>
<p style={{ color: "#666" }}>
{new Date(post.date).toLocaleDateString("he-IL")}
</p>
<p>{post.excerpt}</p>
<Link to={`/posts/${post.id}`}>קרא עוד</Link>
</article>
))}
</div>
);
}
function PostDetail() {
const { postId } = useParams<{ postId: string }>();
const navigate = useNavigate();
const post = posts.find((p) => p.id === Number(postId));
if (!post) {
return (
<div>
<h1>פוסט לא נמצא</h1>
<p>הפוסט עם מספר {postId} לא קיים</p>
<button onClick={() => navigate("/")}>חזרה לרשימה</button>
</div>
);
}
return (
<article>
<button onClick={() => navigate("/")} style={{ marginBottom: "16px" }}>
חזרה לרשימה
</button>
<h1>{post.title}</h1>
<p style={{ color: "#666" }}>
{new Date(post.date).toLocaleDateString("he-IL")}
</p>
<div>{post.content}</div>
</article>
);
}
function NotFound() {
const navigate = useNavigate();
return (
<div>
<h1>404</h1>
<button onClick={() => navigate("/")}>חזרה</button>
</div>
);
}
הסבר:
- ה-URL /posts/:postId תופס את ה-ID מה-URL
- useParams מחזיר את הפרמטר כ-string, לכן ממירים ל-number בהשוואה
- אם הפוסט לא נמצא, מציגים הודעת שגיאה מתאימה
פתרון תרגיל 3 - לוח בקרה עם נתיבים מקוננים¶
import {
BrowserRouter,
Routes,
Route,
NavLink,
Outlet,
useParams,
useLocation,
Link,
} from "react-router-dom";
const users = [
{ id: 1, name: "דני כהן", email: "dani@example.com", role: "מפתח" },
{ id: 2, name: "מיכל לוי", email: "michal@example.com", role: "מעצבת" },
{ id: 3, name: "יוסי אברהם", email: "yossi@example.com", role: "מנהל" },
];
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardOverview />} />
<Route path="users" element={<UsersLayout />}>
<Route index element={<UsersList />} />
<Route path=":userId" element={<UserDetail />} />
</Route>
<Route path="products" element={<Products />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
</BrowserRouter>
);
}
function Breadcrumbs() {
const location = useLocation();
const pathnames = location.pathname.split("/").filter(Boolean);
const labels: Record<string, string> = {
dashboard: "לוח בקרה",
users: "משתמשים",
products: "מוצרים",
settings: "הגדרות",
};
return (
<nav style={{ padding: "8px 0", fontSize: "14px" }}>
{pathnames.map((segment, index) => {
const path = `/${pathnames.slice(0, index + 1).join("/")}`;
const isLast = index === pathnames.length - 1;
const label = labels[segment] || segment;
return (
<span key={path}>
{index > 0 && " / "}
{isLast ? (
<strong>{label}</strong>
) : (
<Link to={path}>{label}</Link>
)}
</span>
);
})}
</nav>
);
}
function DashboardLayout() {
const sidebarLinks = [
{ to: "/dashboard", label: "סקירה", end: true },
{ to: "/dashboard/users", label: "משתמשים", end: false },
{ to: "/dashboard/products", label: "מוצרים", end: false },
{ to: "/dashboard/settings", label: "הגדרות", end: false },
];
return (
<div style={{ display: "flex", minHeight: "100vh" }}>
<aside
style={{
width: "200px",
backgroundColor: "#1e293b",
padding: "20px",
}}
>
<h2 style={{ color: "white" }}>לוח בקרה</h2>
<nav style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
{sidebarLinks.map((link) => (
<NavLink
key={link.to}
to={link.to}
end={link.end}
style={({ isActive }) => ({
color: isActive ? "#3b82f6" : "#94a3b8",
textDecoration: "none",
padding: "8px",
borderRadius: "4px",
backgroundColor: isActive ? "#1e3a5f" : "transparent",
})}
>
{link.label}
</NavLink>
))}
</nav>
</aside>
<main style={{ flex: 1, padding: "20px" }}>
<Breadcrumbs />
<Outlet />
</main>
</div>
);
}
function DashboardOverview() {
return (
<div>
<h1>סקירה כללית</h1>
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: "16px" }}>
<div style={{ padding: "20px", backgroundColor: "#e0f2fe", borderRadius: "8px" }}>
<h3>{users.length}</h3>
<p>משתמשים</p>
</div>
<div style={{ padding: "20px", backgroundColor: "#dcfce7", borderRadius: "8px" }}>
<h3>15</h3>
<p>מוצרים</p>
</div>
<div style={{ padding: "20px", backgroundColor: "#fef3c7", borderRadius: "8px" }}>
<h3>42</h3>
<p>הזמנות</p>
</div>
</div>
</div>
);
}
function UsersLayout() {
return <Outlet />;
}
function UsersList() {
return (
<div>
<h1>משתמשים</h1>
<table style={{ width: "100%", borderCollapse: "collapse" }}>
<thead>
<tr>
<th>שם</th>
<th>אימייל</th>
<th>תפקיד</th>
<th>פעולות</th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr key={user.id} style={{ borderBottom: "1px solid #eee" }}>
<td>{user.name}</td>
<td>{user.email}</td>
<td>{user.role}</td>
<td>
<Link to={`/dashboard/users/${user.id}`}>צפה</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
function UserDetail() {
const { userId } = useParams();
const user = users.find((u) => u.id === Number(userId));
if (!user) return <p>משתמש לא נמצא</p>;
return (
<div>
<Link to="/dashboard/users">חזרה לרשימה</Link>
<h1>{user.name}</h1>
<p>אימייל: {user.email}</p>
<p>תפקיד: {user.role}</p>
</div>
);
}
function Products() {
return <h1>מוצרים</h1>;
}
function Settings() {
return <h1>הגדרות</h1>;
}
הסבר:
- ה-layout הראשי מכיל sidebar קבוע ו-Outlet לתוכן המשתנה
- ה-Breadcrumbs מפרקים את ה-pathname ומציגים כל חלק כקישור
- הנתיבים מקוננים בשתי רמות: dashboard ובתוכו users עם פרטי משתמש
פתרון תרגיל 4 - חנות מקוונת עם סינון ב-URL¶
import {
BrowserRouter,
Routes,
Route,
useSearchParams,
useNavigate,
useParams,
Link,
} from "react-router-dom";
import { useMemo } from "react";
interface Product {
id: number;
name: string;
category: string;
price: number;
}
const products: Product[] = [
{ id: 1, name: "טלפון חכם", category: "electronics", price: 2999 },
{ id: 2, name: "אוזניות", category: "electronics", price: 499 },
{ id: 3, name: "חולצה", category: "clothing", price: 149 },
{ id: 4, name: "מכנסיים", category: "clothing", price: 199 },
{ id: 5, name: "מחשב נייד", category: "electronics", price: 4999 },
{ id: 6, name: "שעון", category: "accessories", price: 899 },
];
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/products" element={<ProductListPage />} />
<Route path="/products/:productId" element={<ProductDetailPage />} />
</Routes>
</BrowserRouter>
);
}
function ProductListPage() {
const [searchParams, setSearchParams] = useSearchParams();
const category = searchParams.get("category") || "all";
const sort = searchParams.get("sort") || "name";
const minPrice = Number(searchParams.get("minPrice")) || 0;
const maxPrice = Number(searchParams.get("maxPrice")) || Infinity;
const updateFilter = (key: string, value: string) => {
setSearchParams((prev) => {
if (value === "" || value === "all" || value === "0") {
prev.delete(key);
} else {
prev.set(key, value);
}
return prev;
});
};
const clearFilters = () => {
setSearchParams({});
};
const filtered = useMemo(() => {
let result = products.filter((p) => {
if (category !== "all" && p.category !== category) return false;
if (p.price < minPrice) return false;
if (maxPrice !== Infinity && p.price > maxPrice) return false;
return true;
});
result.sort((a, b) => {
if (sort === "price-asc") return a.price - b.price;
if (sort === "price-desc") return b.price - a.price;
return a.name.localeCompare(b.name);
});
return result;
}, [category, sort, minPrice, maxPrice]);
return (
<div>
<h1>מוצרים</h1>
<div style={{ display: "flex", gap: "12px", marginBottom: "20px" }}>
<select value={category} onChange={(e) => updateFilter("category", e.target.value)}>
<option value="all">כל הקטגוריות</option>
<option value="electronics">אלקטרוניקה</option>
<option value="clothing">ביגוד</option>
<option value="accessories">אביזרים</option>
</select>
<select value={sort} onChange={(e) => updateFilter("sort", e.target.value)}>
<option value="name">מיין לפי שם</option>
<option value="price-asc">מחיר - מהנמוך לגבוה</option>
<option value="price-desc">מחיר - מהגבוה לנמוך</option>
</select>
<input
type="number"
placeholder="מחיר מינימלי"
value={minPrice || ""}
onChange={(e) => updateFilter("minPrice", e.target.value)}
/>
<input
type="number"
placeholder="מחיר מקסימלי"
value={maxPrice === Infinity ? "" : maxPrice}
onChange={(e) => updateFilter("maxPrice", e.target.value)}
/>
<button onClick={clearFilters}>נקה פילטרים</button>
</div>
<p>מציג {filtered.length} מוצרים</p>
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: "16px" }}>
{filtered.map((product) => (
<div key={product.id} style={{ border: "1px solid #ddd", padding: "16px" }}>
<h3>{product.name}</h3>
<p>{product.price} ש"ח</p>
<Link to={`/products/${product.id}?${searchParams.toString()}`}>
פרטים
</Link>
</div>
))}
</div>
</div>
);
}
function ProductDetailPage() {
const { productId } = useParams();
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const product = products.find((p) => p.id === Number(productId));
if (!product) return <p>מוצר לא נמצא</p>;
return (
<div>
<button onClick={() => navigate(`/products?${searchParams.toString()}`)}>
חזרה למוצרים
</button>
<h1>{product.name}</h1>
<p>קטגוריה: {product.category}</p>
<p>מחיר: {product.price} ש"ח</p>
</div>
);
}
הסبر:
- כל הפילטרים נשמרים ב-URL כ-query parameters
- בניקוי פילטר (ערך ריק), המפתח נמחק מה-URL
- כשעוברים לדף מוצר, שומרים את ה-search params בלינק כדי שהכפתור "חזרה" ישמור את הפילטרים
פתרון תרגיל 5 - אותנטיקציה עם נתיבים מוגנים¶
import {
BrowserRouter,
Routes,
Route,
Navigate,
Outlet,
useNavigate,
useLocation,
NavLink,
} from "react-router-dom";
import { createContext, useContext, useState } from "react";
interface AuthContextType {
user: { name: string; email: string } | null;
login: (email: string, password: string) => boolean;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | null>(null);
function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be used within AuthProvider");
return ctx;
}
function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<{ name: string; email: string } | null>(null);
const login = (email: string, password: string) => {
if (password.length >= 4) {
setUser({ name: "משתמש", email });
return true;
}
return false;
};
const logout = () => setUser(null);
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
function ProtectedRoute() {
const { user } = useAuth();
const location = useLocation();
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <Outlet />;
}
function GuestRoute() {
const { user } = useAuth();
if (user) {
return <Navigate to="/" replace />;
}
return <Outlet />;
}
function App() {
return (
<BrowserRouter>
<AuthProvider>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route element={<GuestRoute />}>
<Route path="login" element={<LoginPage />} />
<Route path="register" element={<RegisterPage />} />
</Route>
<Route element={<ProtectedRoute />}>
<Route path="profile" element={<ProfilePage />} />
<Route path="settings" element={<SettingsPage />} />
<Route path="dashboard" element={<DashboardPage />} />
</Route>
</Route>
</Routes>
</AuthProvider>
</BrowserRouter>
);
}
function Layout() {
const { user, logout } = useAuth();
const navigate = useNavigate();
return (
<div>
<nav style={{ display: "flex", gap: "12px", padding: "16px", backgroundColor: "#f5f5f5" }}>
<NavLink to="/">בית</NavLink>
{user ? (
<>
<NavLink to="/profile">פרופיל</NavLink>
<NavLink to="/dashboard">לוח בקרה</NavLink>
<NavLink to="/settings">הגדרות</NavLink>
<button onClick={() => { logout(); navigate("/"); }}>התנתק</button>
</>
) : (
<>
<NavLink to="/login">התחבר</NavLink>
<NavLink to="/register">הירשם</NavLink>
</>
)}
</nav>
<main style={{ padding: "20px" }}>
<Outlet />
</main>
</div>
);
}
function Home() {
return <h1>דף הבית</h1>;
}
function LoginPage() {
const { login } = useAuth();
const navigate = useNavigate();
const location = useLocation();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const from = (location.state as any)?.from?.pathname || "/";
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (login(email, password)) {
navigate(from, { replace: true });
} else {
setError("אימייל או סיסמה שגויים");
}
};
return (
<form onSubmit={handleSubmit}>
<h1>התחברות</h1>
{from !== "/" && <p>עליך להתחבר כדי לגשת ל-{from}</p>}
{error && <p style={{ color: "red" }}>{error}</p>}
<input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="אימייל" />
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="סיסמה" />
<button type="submit">התחבר</button>
</form>
);
}
function RegisterPage() {
return <h1>הרשמה</h1>;
}
function ProfilePage() {
const { user } = useAuth();
return (
<div>
<h1>פרופיל</h1>
<p>שם: {user?.name}</p>
<p>אימייל: {user?.email}</p>
</div>
);
}
function SettingsPage() {
return <h1>הגדרות</h1>;
}
function DashboardPage() {
return <h1>לוח בקרה</h1>;
}
הסבר:
- GuestRoute מפנה משתמשים מחוברים לדף הבית (אין טעם שיראו דף התחברות)
- ProtectedRoute שומר את המיקום הנוכחי ב-state של Navigate
- בדף התחברות, קוראים את from מ-state ומפנים אליו אחרי התחברות מוצלחת
פתרון תרגיל 6 - אפליקציית מתכונים מלאה¶
הפתרון מורכב מהרבה חלקים, נציג את המבנה העיקרי:
import {
BrowserRouter,
Routes,
Route,
NavLink,
Outlet,
useParams,
useSearchParams,
useNavigate,
Navigate,
Link,
useLocation,
} from "react-router-dom";
import { useState, useMemo, createContext, useContext } from "react";
interface Recipe {
id: number;
title: string;
category: string;
description: string;
ingredients: string[];
time: number;
}
const recipes: Recipe[] = [
{ id: 1, title: "פסטה ברוטב עגבניות", category: "איטלקי", description: "פסטה קלאסית", ingredients: ["פסטה", "עגבניות", "שום"], time: 30 },
{ id: 2, title: "חומוס ביתי", category: "ישראלי", description: "חומוס אמיתי", ingredients: ["חומוס", "טחינה", "לימון"], time: 60 },
{ id: 3, title: "סושי", category: "יפני", description: "סושי טרי", ingredients: ["אורז", "דג", "נורי"], time: 45 },
{ id: 4, title: "שקשוקה", category: "ישראלי", description: "שקשוקה חריפה", ingredients: ["עגבניות", "ביצים", "פלפל"], time: 25 },
];
// קונטקסט מועדפים
const FavoritesContext = createContext<{
favorites: number[];
toggleFavorite: (id: number) => void;
isFavorite: (id: number) => boolean;
} | null>(null);
function useFavorites() {
const ctx = useContext(FavoritesContext);
if (!ctx) throw new Error("useFavorites requires FavoritesProvider");
return ctx;
}
function FavoritesProvider({ children }: { children: React.ReactNode }) {
const [favorites, setFavorites] = useState<number[]>(() => {
const saved = localStorage.getItem("favorites");
return saved ? JSON.parse(saved) : [];
});
const toggleFavorite = (id: number) => {
setFavorites((prev) => {
const next = prev.includes(id)
? prev.filter((f) => f !== id)
: [...prev, id];
localStorage.setItem("favorites", JSON.stringify(next));
return next;
});
};
const isFavorite = (id: number) => favorites.includes(id);
return (
<FavoritesContext.Provider value={{ favorites, toggleFavorite, isFavorite }}>
{children}
</FavoritesContext.Provider>
);
}
function App() {
return (
<BrowserRouter>
<FavoritesProvider>
<Routes>
<Route path="/" element={<AppLayout />}>
<Route index element={<HomePage />} />
<Route path="recipes" element={<RecipesPage />} />
<Route path="recipes/:id" element={<RecipeDetailPage />} />
<Route path="categories/:category" element={<CategoryPage />} />
<Route path="favorites" element={<FavoritesPage />} />
<Route path="*" element={<NotFoundPage />} />
</Route>
</Routes>
</FavoritesProvider>
</BrowserRouter>
);
}
function Breadcrumbs() {
const location = useLocation();
const parts = location.pathname.split("/").filter(Boolean);
const labels: Record<string, string> = {
recipes: "מתכונים",
categories: "קטגוריות",
favorites: "מועדפים",
};
return (
<nav style={{ padding: "8px 0", fontSize: "14px" }}>
<Link to="/">בית</Link>
{parts.map((part, i) => {
const path = `/${parts.slice(0, i + 1).join("/")}`;
const label = labels[part] || decodeURIComponent(part);
const isLast = i === parts.length - 1;
return (
<span key={path}>
{" / "}
{isLast ? <strong>{label}</strong> : <Link to={path}>{label}</Link>}
</span>
);
})}
</nav>
);
}
function AppLayout() {
return (
<div>
<nav style={{ display: "flex", gap: "16px", padding: "16px", backgroundColor: "#f8f8f8" }}>
<NavLink to="/">בית</NavLink>
<NavLink to="/recipes">מתכונים</NavLink>
<NavLink to="/favorites">מועדפים</NavLink>
</nav>
<main style={{ padding: "20px" }}>
<Breadcrumbs />
<Outlet />
</main>
</div>
);
}
function RecipesPage() {
const [searchParams, setSearchParams] = useSearchParams();
const search = searchParams.get("search") || "";
const category = searchParams.get("category") || "all";
const filtered = useMemo(() => {
return recipes.filter((r) => {
if (category !== "all" && r.category !== category) return false;
if (search && !r.title.includes(search)) return false;
return true;
});
}, [search, category]);
return (
<div>
<h1>מתכונים</h1>
<input
value={search}
onChange={(e) => {
setSearchParams((prev) => {
if (e.target.value) prev.set("search", e.target.value);
else prev.delete("search");
return prev;
});
}}
placeholder="חפש מתכון..."
/>
<div>
{filtered.map((recipe) => (
<RecipeCard key={recipe.id} recipe={recipe} />
))}
</div>
</div>
);
}
function RecipeCard({ recipe }: { recipe: Recipe }) {
const { toggleFavorite, isFavorite } = useFavorites();
return (
<div style={{ border: "1px solid #ddd", padding: "16px", margin: "8px 0" }}>
<Link to={`/recipes/${recipe.id}`}><h3>{recipe.title}</h3></Link>
<p>{recipe.description}</p>
<p>זמן הכנה: {recipe.time} דקות</p>
<Link to={`/categories/${recipe.category}`}>{recipe.category}</Link>
<button onClick={() => toggleFavorite(recipe.id)}>
{isFavorite(recipe.id) ? "הסר ממועדפים" : "הוסף למועדפים"}
</button>
</div>
);
}
function RecipeDetailPage() {
const { id } = useParams();
const navigate = useNavigate();
const recipe = recipes.find((r) => r.id === Number(id));
if (!recipe) {
return (
<div>
<h1>מתכון לא נמצא</h1>
<button onClick={() => navigate("/recipes")}>חזרה למתכונים</button>
</div>
);
}
return (
<div>
<button onClick={() => navigate(-1)}>חזרה</button>
<h1>{recipe.title}</h1>
<p>{recipe.description}</p>
<p>זמן הכנה: {recipe.time} דקות</p>
<h3>מצרכים:</h3>
<ul>
{recipe.ingredients.map((ing, i) => (
<li key={i}>{ing}</li>
))}
</ul>
</div>
);
}
function CategoryPage() {
const { category } = useParams();
const filtered = recipes.filter((r) => r.category === category);
return (
<div>
<h1>קטגוריה: {category}</h1>
{filtered.map((recipe) => (
<RecipeCard key={recipe.id} recipe={recipe} />
))}
</div>
);
}
function FavoritesPage() {
const { favorites } = useFavorites();
const favoriteRecipes = recipes.filter((r) => favorites.includes(r.id));
return (
<div>
<h1>המועדפים שלי</h1>
{favoriteRecipes.length === 0 ? (
<p>אין מועדפים עדיין. <Link to="/recipes">עבור למתכונים</Link></p>
) : (
favoriteRecipes.map((recipe) => (
<RecipeCard key={recipe.id} recipe={recipe} />
))
)}
</div>
);
}
function HomePage() {
return (
<div>
<h1>ברוכים הבאים לאפליקציית המתכונים</h1>
<Link to="/recipes">צפו בכל המתכונים</Link>
</div>
);
}
function NotFoundPage() {
return (
<div style={{ textAlign: "center" }}>
<h1>404 - דף לא נמצא</h1>
<Link to="/">חזרה לדף הבית</Link>
</div>
);
}
הסبר:
- מועדפים מנוהלים בקונטקסט נפרד עם שמירה ב-localStorage
- חיפוש וסינון נשמרים ב-URL דרך useSearchParams
- Breadcrumbs מייצרים ניווט דינמי לפי ה-pathname הנוכחי
- כל דף מתכון ודף קטגוריה מטפלים במקרה שהנתון לא נמצא
תשובות לשאלות¶
-
Link מול NavLink: Link הוא קישור בסיסי שמונע refresh. NavLink מוסיף מודעות למצב פעיל - יודע אם הנתיב שלו תואם ל-URL הנוכחי. נשתמש ב-NavLink בתפריטי ניווט שרוצים להדגיש את הדף הנוכחי, וב-Link בקישורים רגילים.
-
navigate() מול Navigate:
navigate()(מ-useNavigate) הוא פונקציה שנקראת מתוך event handlers או logic.Navigateהוא קומפוננטה שמבצעת ניווט כחלק מהרנדר (declarative). נשתמש ב-navigate() ב-onClick, handleSubmit וכדומה, וב-Navigate בתוך JSX לניתובים מותנים (כמו ProtectedRoute). -
useSearchParams מול useState: יתרונות URL: אפשר לשתף את הלינק עם הפילטרים, refresh שומר את המצב, כפתורי חזרה/קדימה בדפדפן עובדים, ו-SEO טוב יותר. חסרונות: מוגבל לנתונים שניתן לייצג כ-string ב-URL, ופחות מתאים למצבים מורכבים.
-
איך Outlet עובד: Outlet הוא placeholder שבו ריאקט ראוטר מרנדר את הנתיב הילד המתאים. ברמות מקוננות, כל Outlet מרנדר את הרמה הבאה. לדוגמה: Layout Outlet מרנדר Dashboard, ו-Dashboard Outlet מרנדר Analytics. זה מאפשר layouts משותפים בכל רמה.
-
replace: true מול false: בברירת מחדל (false), הניווט מוסיף כניסה חדשה להיסטוריה - המשתמש יכול ללחוץ "חזרה" ולחזור. עם replace: true, הנתיב הנוכחי מוחלף ואין אפשרות לחזור אליו. שימושי אחרי התחברות (אין טעם לחזור לדף login), אחרי redirect, ובניווטים שלא צריכים להישמר בהיסטוריה.