לדלג לתוכן

7.4 ריאקט ראוטר הרצאה

ריאקט ראוטר - React Router

בשיעור הזה נלמד על ניווט בצד הלקוח (Client-Side Routing) באמצעות React Router - הספרייה הפופולרית ביותר לניתוב באפליקציות ריאקט.


ניתוב בצד הלקוח - Client-Side Routing

מה זה?

  • באפליקציות רגילות (MPA - Multi Page Application), כל מעבר בין דפים גורם לטעינה מחדש מהשרת
  • באפליקציות SPA (Single Page Application), כל הקוד נטען פעם אחת, והניווט מתבצע בצד הלקוח
  • React Router מנהל את ה-URL ומציג את הקומפוננטה המתאימה בלי לרענן את הדף

התקנה

npm install react-router-dom

הגדרה בסיסית

הקומפוננטה 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)

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 מגדיר את הנתיב היעד
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