7.4 ריאקט ראוטר הרצאה
ריאקט ראוטר - React Router¶
בשיעור הזה נלמד על ניווט בצד הלקוח (Client-Side Routing) באמצעות React Router - הספרייה הפופולרית ביותר לניתוב באפליקציות ריאקט.
ניתוב בצד הלקוח - Client-Side Routing¶
מה זה?¶
- באפליקציות רגילות (MPA - Multi Page Application), כל מעבר בין דפים גורם לטעינה מחדש מהשרת
- באפליקציות SPA (Single Page Application), כל הקוד נטען פעם אחת, והניווט מתבצע בצד הלקוח
- React Router מנהל את ה-URL ומציג את הקומפוננטה המתאימה בלי לרענן את הדף
התקנה¶
הגדרה בסיסית¶
הקומפוננטה BrowserRouter¶
import { BrowserRouter, Routes, Route } from "react-router-dom";
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</BrowserRouter>
);
}
function Home() {
return <h1>דף הבית</h1>;
}
function About() {
return <h1>אודות</h1>;
}
function Contact() {
return <h1>צרו קשר</h1>;
}
BrowserRouterעוטף את כל האפליקציה ומספק את ההקשר של הניתובRoutesמגדיר קבוצה של נתיביםRouteממפה נתיב (path) לקומפוננטה (element)
ניווט - Link ו-NavLink¶
הקומפוננטה Link¶
import { Link } from "react-router-dom";
function Navigation() {
return (
<nav>
<Link to="/">בית</Link>
<Link to="/about">אודות</Link>
<Link to="/contact">צרו קשר</Link>
</nav>
);
}
Linkמחליף את תגית<a>ומונע טעינה מחדש של הדף- הפרופ
toמגדיר את הנתיב היעד
הקומפוננטה NavLink¶
import { NavLink } from "react-router-dom";
function Navigation() {
return (
<nav>
<NavLink
to="/"
className={({ isActive }) => (isActive ? "active" : "")}
>
בית
</NavLink>
<NavLink
to="/about"
style={({ isActive }) => ({
fontWeight: isActive ? "bold" : "normal",
color: isActive ? "blue" : "black",
})}
>
אודות
</NavLink>
</nav>
);
}
NavLinkהוא כמוLinkאבל עם מודעות לנתיב הפעיל- מקבל פונקציה ב-className או style שמקבלת אובייקט עם
isActive
פרמטרים דינמיים - URL Parameters¶
הגדרת נתיב דינמי¶
import { Routes, Route } from "react-router-dom";
function App() {
return (
<Routes>
<Route path="/users" element={<UserList />} />
<Route path="/users/:userId" element={<UserProfile />} />
<Route path="/users/:userId/posts/:postId" element={<UserPost />} />
</Routes>
);
}
קריאת הפרמטרים עם useParams¶
import { useParams } from "react-router-dom";
function UserProfile() {
const { userId } = useParams<{ userId: string }>();
return <h1>פרופיל משתמש: {userId}</h1>;
}
function UserPost() {
const { userId, postId } = useParams<{
userId: string;
postId: string;
}>();
return (
<div>
<h1>פוסט {postId} של משתמש {userId}</h1>
</div>
);
}
ניווט תכנותי - useNavigate¶
import { useNavigate } from "react-router-dom";
function LoginForm() {
const navigate = useNavigate();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const success = await login();
if (success) {
navigate("/dashboard");
}
};
return (
<form onSubmit={handleSubmit}>
<button type="submit">התחבר</button>
</form>
);
}
function NavigationExamples() {
const navigate = useNavigate();
return (
<div>
<button onClick={() => navigate("/about")}>עבור לאודות</button>
<button onClick={() => navigate(-1)}>חזרה</button>
<button onClick={() => navigate(1)}>קדימה</button>
<button onClick={() => navigate("/login", { replace: true })}>
עבור לכניסה (ללא היסטוריה)
</button>
<button
onClick={() =>
navigate("/profile", { state: { from: "home" } })
}
>
עבור לפרופיל עם נתונים
</button>
</div>
);
}
navigate(path)- מעביר לנתיב חדשnavigate(-1)- חוזר אחורה בהיסטוריהreplace: true- מחליף את הנתיב הנוכחי במקום להוסיף להיסטוריהstate- מעביר נתונים נוספים עם הניווט
פרמטרי חיפוש - Search Parameters¶
import { useSearchParams } from "react-router-dom";
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams();
const category = searchParams.get("category") || "all";
const sort = searchParams.get("sort") || "name";
const page = Number(searchParams.get("page")) || 1;
const updateFilters = (key: string, value: string) => {
setSearchParams((prev) => {
prev.set(key, value);
return prev;
});
};
return (
<div>
<p>קטגוריה: {category}, מיון: {sort}, עמוד: {page}</p>
<select
value={category}
onChange={(e) => updateFilters("category", e.target.value)}
>
<option value="all">הכל</option>
<option value="electronics">אלקטרוניקה</option>
<option value="clothing">ביגוד</option>
</select>
<button onClick={() => updateFilters("page", String(page + 1))}>
עמוד הבא
</button>
</div>
);
}
- useSearchParams עובד כמו useState אבל עם query string ב-URL
- מאפשר שמירת מצב ב-URL (שיתוף לינקים, refresh)
נתיבים מקוננים - Nested Routes¶
import { Routes, Route, Outlet, NavLink } from "react-router-dom";
function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="dashboard" element={<Dashboard />}>
<Route index element={<DashboardHome />} />
<Route path="analytics" element={<Analytics />} />
<Route path="settings" element={<Settings />} />
</Route>
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
);
}
function Layout() {
return (
<div>
<nav>
<NavLink to="/">בית</NavLink>
<NavLink to="/dashboard">לוח בקרה</NavLink>
</nav>
<main>
<Outlet />
</main>
<footer>כל הזכויות שמורות</footer>
</div>
);
}
function Dashboard() {
return (
<div>
<h1>לוח בקרה</h1>
<nav>
<NavLink to="/dashboard">ראשי</NavLink>
<NavLink to="/dashboard/analytics">אנליטיקס</NavLink>
<NavLink to="/dashboard/settings">הגדרות</NavLink>
</nav>
<div>
<Outlet />
</div>
</div>
);
}
function DashboardHome() {
return <p>ברוכים הבאים ללוח הבקרה</p>;
}
function Analytics() {
return <p>דף אנליטיקס</p>;
}
function Settings() {
return <p>דף הגדרות</p>;
}
Outletהוא placeholder שבו ירונדר הנתיב הילד הפעילindexמגדיר את הנתיב שירונדר כברירת מחדל (כשהנתיב מתאים בדיוק להורה)path="*"תופס כל נתיב שלא התאים - מושלם לדף 404
דף 404¶
function NotFound() {
const navigate = useNavigate();
return (
<div style={{ textAlign: "center", padding: "50px" }}>
<h1>404</h1>
<p>הדף שחיפשת לא נמצא</p>
<button onClick={() => navigate("/")}>חזרה לדף הבית</button>
</div>
);
}
נתיבים מוגנים - Protected Routes¶
import { Navigate, Outlet, useLocation } from "react-router-dom";
// הנחה שיש הוק useAuth שמנהל אותנטיקציה
function ProtectedRoute() {
const { state } = useAuth();
const location = useLocation();
if (state.status === "loading") {
return <p>טוען...</p>;
}
if (state.status !== "authenticated") {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <Outlet />;
}
function RoleProtectedRoute({ requiredRole }: { requiredRole: string }) {
const { state } = useAuth();
if (state.user?.role !== requiredRole) {
return <Navigate to="/unauthorized" replace />;
}
return <Outlet />;
}
// שימוש
function App() {
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route element={<ProtectedRoute />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
<Route element={<RoleProtectedRoute requiredRole="admin" />}>
<Route path="/admin" element={<AdminPanel />} />
</Route>
</Route>
<Route path="/unauthorized" element={<Unauthorized />} />
<Route path="*" element={<NotFound />} />
</Routes>
);
}
Navigateמבצע ניווט אוטומטי (redirect)- שומרים את המיקום הנוכחי ב-state כדי לחזור אליו אחרי התחברות
- נתיבים מוגנים יכולים להיות מקוננים - כאן דף admin דורש גם אותנטיקציה וגם role מתאים
חזרה לדף המקורי אחרי התחברות¶
function LoginPage() {
const navigate = useNavigate();
const location = useLocation();
const { login } = useAuth();
const from = (location.state as { from?: Location })?.from?.pathname || "/";
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await login(email, password);
navigate(from, { replace: true });
};
return (
<form onSubmit={handleSubmit}>
<p>עליך להתחבר כדי לגשת לדף {from}</p>
{/* שדות טופס */}
</form>
);
}
דוגמה מסכמת - אפליקציה מלאה¶
import {
BrowserRouter,
Routes,
Route,
NavLink,
Outlet,
useParams,
useNavigate,
useSearchParams,
Navigate,
} from "react-router-dom";
function App() {
return (
<BrowserRouter>
<AuthProvider>
<Routes>
<Route path="/" element={<MainLayout />}>
<Route index element={<Home />} />
<Route path="products" element={<ProductsLayout />}>
<Route index element={<ProductList />} />
<Route path=":productId" element={<ProductDetail />} />
</Route>
<Route path="login" element={<Login />} />
<Route element={<ProtectedRoute />}>
<Route path="account" element={<Account />} />
<Route path="orders" element={<Orders />} />
</Route>
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</AuthProvider>
</BrowserRouter>
);
}
function MainLayout() {
return (
<div>
<header>
<nav>
<NavLink to="/">בית</NavLink>
<NavLink to="/products">מוצרים</NavLink>
<NavLink to="/account">החשבון שלי</NavLink>
</nav>
</header>
<main>
<Outlet />
</main>
</div>
);
}
function ProductsLayout() {
return (
<div>
<h1>מוצרים</h1>
<Outlet />
</div>
);
}
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams();
const category = searchParams.get("category") || "all";
return (
<div>
<select
value={category}
onChange={(e) =>
setSearchParams({ category: e.target.value })
}
>
<option value="all">כל הקטגוריות</option>
<option value="phones">טלפונים</option>
<option value="laptops">מחשבים ניידים</option>
</select>
<NavLink to="/products/1">מוצר 1</NavLink>
<NavLink to="/products/2">מוצר 2</NavLink>
</div>
);
}
function ProductDetail() {
const { productId } = useParams();
const navigate = useNavigate();
return (
<div>
<button onClick={() => navigate(-1)}>חזרה</button>
<h2>מוצר מספר {productId}</h2>
</div>
);
}
סיכום¶
- React Router מספק ניתוב בצד הלקוח ללא טעינה מחדש של הדף
- BrowserRouter עוטף את האפליקציה, Routes ו-Route מגדירים את הנתיבים
- Link ו-NavLink מספקים ניווט ללא refresh, כש-NavLink מודע לנתיב הפעיל
- useParams קורא פרמטרים דינמיים מה-URL
- useSearchParams מנהל query string ב-URL
- useNavigate מאפשר ניווט תכנותי
- נתיבים מקוננים עם Outlet מאפשרים layouts משותפים
- Protected Routes מגבילים גישה לנתיבים ספציפיים
- path="*" תופס נתיבים לא מוכרים לדף 404