ניתוב עם React Router

מבוא

React עצמה מטפלת ברכיב בודד, ולא בניווט בין דפים. כאן נכנסת React Router - ספריית הניתוב (Routing) הנפוצה ביותר לאפליקציות React. היא מאפשרת לבנות אפליקציית עמוד יחיד (SPA) שמתנהגת כאילו יש בה כמה דפים: הכתובת בשורת הדפדפן משתנה, אך במקום לטעון מחדש את כל העמוד, React Router פשוט מחליפה את הקומפוננטה המוצגת. התוצאה היא מעבר מהיר וחלק בין מסכים.

התקנה

npm install react-router-dom

הגדרה בסיסית

import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';

function App() {
    return (
        <BrowserRouter>
            {/* תפריט ניווט */}
            <nav>
                <Link to="/">בית</Link>
                <Link to="/about">אודות</Link>
                <Link to="/contact">צור קשר</Link>
            </nav>

            {/* הגדרת נתיבים */}
            <Routes>
                <Route path="/"         element={<Home />} />
                <Route path="/about"    element={<About />} />
                <Route path="/contact"  element={<Contact />} />
                <Route path="*"         element={<NotFound />} />
            </Routes>
        </BrowserRouter>
    );
}

function Home() {
    return <h1>דף הבית</h1>;
}

function About() {
    return <h1>אודות</h1>;
}

function Contact() {
    return <h1>צור קשר</h1>;
}

function NotFound() {
    return <h1>404 - הדף לא נמצא</h1>;
}

import { Link } from 'react-router-dom';

function Navigation() {
    return (
        <nav>
            <Link to="/">בית</Link>
            <Link to="/products">מוצרים</Link>
            <Link to="/about">אודות</Link>
        </nav>
    );
}
import { NavLink } from 'react-router-dom';

function Navigation() {
    return (
        <nav>
            <NavLink 
                to="/"
                className={({ isActive }) => isActive ? 'active' : ''}
            >
                בית
            </NavLink>

            <NavLink 
                to="/products"
                style={({ isActive }) => ({
                    fontWeight: isActive ? 'bold' : 'normal',
                    color: isActive ? 'blue' : 'black'
                })}
            >
                מוצרים
            </NavLink>
        </nav>
    );
}

Dynamic Routes (נתיבים דינמיים)

לעיתים קרובות נרצה נתיב שמשתנה לפי נתון - למשל עמוד פרופיל נפרד לכל משתמש. במקום להגדיר נתיב לכל אחד, מגדירים נתיב אחד עם פרמטר דינמי (המסומן בנקודתיים, למשל :userId), וקוראים את ערכו בעזרת ה-Hook בשם useParams:

הגדרת פרמטרים

function App() {
    return (
        <Routes>
            <Route path="/users"                        element={<UserList />} />
            <Route path="/users/:userId"                element={<UserProfile />} />
            <Route path="/products/:category/:productId"element={<Product />} />
        </Routes>
    );
}

קריאת פרמטרים עם useParams

import { useParams } from 'react-router-dom';

function UserProfile() {
    const { userId } = useParams();

    return <h1>פרופיל משתמש: {userId}</h1>;
}

function Product() {
    const { category, productId } = useParams();

    return (
        <div>
            <h1>קטגוריה: {category}</h1>
            <h2>מוצר: {productId}</h2>
        </div>
    );
}

דוגמה מלאה

function UserList() {
    const users = [
        { id: 1, name: 'דני' },
        { id: 2, name: 'רונית' },
        { id: 3, name: 'משה' }
    ];

    return (
        <div>
            <h1>רשימת משתמשים</h1>
            <ul>
                {users.map(user => (
                    <li key={user.id}>
                        <Link to={`/users/${user.id}`}>{user.name}</Link>
                    </li>
                ))}
            </ul>
        </div>
    );
}

function UserProfile() {
    const { userId } = useParams();
    const [user, setUser] = useState(null);

    useEffect(() => {
        fetch(`/api/users/${userId}`)
            .then(res => res.json())
            .then(setUser);
    }, [userId]);

    if (!user) return <p>טוען...</p>;

    return (
        <div>
            <h1>{user.name}</h1>
            <p>אימייל: {user.email}</p>
        </div>
    );
}

Nested Routes (נתיבים מקוננים)

נתיבים מקוננים מאפשרים לשתף מבנה (Layout) בין כמה מסכים - למשל תפריט וכותרת קבועים, שבתוכם מתחלף רק התוכן. מגדירים נתיבי ילד בתוך נתיב הורה, וקומפוננטת ההורה מציינת בעזרת <Outlet /> היכן יוצג הנתיב הפעיל:

function App() {
    return (
        <Routes>
            <Route path="/"                 element={<Layout />}>
                <Route index                element={<Home />} />
                <Route path="about"         element={<About />} />
                <Route path="dashboard"     element={<Dashboard />}>
                    <Route index            element={<DashboardHome />} />
                    <Route path="profile"   element={<Profile />} />
                    <Route path="settings"  element={<Settings />} />
                </Route>
            </Route>
        </Routes>
    );
}

Outlet - הצגת נתיב ילד

import { Outlet, Link } from 'react-router-dom';

function Layout() {
    return (
        <div>
            <header>
                <nav>
                    <Link to="/">בית</Link>
                    <Link to="/dashboard">Dashboard</Link>
                </nav>
            </header>

            <main>
                <Outlet /> {/* כאן יוצג הנתיב הילד */}
            </main>

            <footer>פוטר</footer>
        </div>
    );
}

function Dashboard() {
    return (
        <div className="dashboard">
            <aside>
                <nav>
                    <Link to="/dashboard">סקירה</Link>
                    <Link to="/dashboard/profile">פרופיל</Link>
                    <Link to="/dashboard/settings">הגדרות</Link>
                </nav>
            </aside>

            <div className="content">
                <Outlet /> {/* תתי-נתיבים */}
            </div>
        </div>
    );
}

Query Parameters

פרמטרים של שאילתה (החלק שאחרי ה-? בכתובת, למשל ?q=react&page=2) מתאימים לנתונים אופציונליים כמו מונחי חיפוש, סינון ומספר עמוד. ה-Hook בשם useSearchParams מאפשר לקרוא ולעדכן אותם בדומה ל-useState:

import { useSearchParams } from 'react-router-dom';

function SearchResults() {
    const [searchParams, setSearchParams] = useSearchParams();

    // קריאה
    const query = searchParams.get('q');
    const page = searchParams.get('page') || '1';
    const sort = searchParams.get('sort') || 'date';

    // עדכון
    const updateSearch = (newQuery) => {
        setSearchParams({ q: newQuery, page: '1' });
    };

    const nextPage = () => {
        setSearchParams({
            q: query,
            page: String(Number(page) + 1),
            sort
        });
    };

    return (
        <div>
            <input 
                value={query || ''}
                onChange={(e) => updateSearch(e.target.value)}
                placeholder="חפש..."
            />
            <p>מציג תוצאות עבור: "{query}"</p>
            <p>עמוד: {page}</p>
            <button onClick={nextPage}>עמוד הבא</button>
        </div>
    );
}

Programmatic Navigation

לצד ניווט בלחיצה על קישור, לעיתים צריך לנווט מתוך הקוד עצמו - למשל להעביר את המשתמש ללוח הבקרה מיד לאחר התחברות מוצלחת. לשם כך משתמשים ב-Hook בשם useNavigate:

import { useNavigate } from 'react-router-dom';

function LoginForm() {
    const navigate = useNavigate();

    const handleSubmit = async (e) => {
        e.preventDefault();

        const success = await login(email, password);

        if (success) {
            // ניווט לאחר התחברות
            navigate('/dashboard');

            // או עם replace (לא נשמר בהיסטוריה)
            navigate('/dashboard', { replace: true });

            // או אחורה
            navigate(-1);
        }
    };

    return <form onSubmit={handleSubmit}>...</form>;
}

Protected Routes

נתיבים מוגנים חוסמים גישה למסכים מסוימים ממשתמשים שאינם מחוברים. מממשים זאת בעזרת קומפוננטת עטיפה שבודקת את מצב ההתחברות, ומפנה ל-login אם צריך - תוך שמירת היעד המקורי כדי לחזור אליו לאחר ההתחברות:

import { Navigate, useLocation } from 'react-router-dom';

function ProtectedRoute({ children }) {
    const { isAuthenticated, loading } = useAuth();
    const location = useLocation();

    if (loading) {
        return <p>טוען...</p>;
    }

    if (!isAuthenticated) {
        // שמירת המיקום המבוקש לאחר התחברות
        return <Navigate to="/login" state={{ from: location }} replace />;
    }

    return children;
}

// שימוש
function App() {
    return (
        <Routes>
            <Route path="/login" element={<Login />} />

            <Route 
                path="/dashboard" 
                element={
                    <ProtectedRoute>
                        <Dashboard />
                    </ProtectedRoute>
                }
            />
        </Routes>
    );
}

// ניווט חזרה אחרי התחברות
function Login() {
    const navigate = useNavigate();
    const location = useLocation();

    const from = location.state?.from?.pathname || '/';

    const handleLogin = async () => {
        await login();
        navigate(from, { replace: true });
    };
}

useLocation Hook

import { useLocation } from 'react-router-dom';

function CurrentPath() {
    const location = useLocation();

    // location.pathname = '/users/123'
    // location.search = '?tab=profile'
    // location.hash = '#section1'
    // location.state = { from: ... }

    return (
        <div>
            <p>נתיב: {location.pathname}</p>
            <p>Query: {location.search}</p>
        </div>
    );
}

סיכום

Component/Hook שימוש
BrowserRouter עוטף את האפליקציה
Routes מכיל את הנתיבים
Route הגדרת נתיב בודד
Link קישור לניווט
NavLink קישור עם active state
Outlet הצגת nested route
useParams קריאת פרמטרים דינמיים
useSearchParams query string
useNavigate ניווט בקוד
useLocation מידע על הנתיב הנוכחי

הקודם: ← React Patterns
הבא: Performance Optimization →

בדקו את עצמכם

נסו לענות לבד לפני שאתם פותחים את התשובה.

  1. למה משתמשים ב-<Link> ולא ב-<a href> בתוך האפליקציה?

    1. עיצוב
    2. Link מחליף דף בלי לטעון את כל האתר מחדש, ושומר את ה-state
    3. a לא עובד ב-React
    4. Link מהיר בשרת
    הצגת התשובה

    תשובה ב. זה מה שהופך את האפליקציה ל-SPA.

  2. איך קוראים פרמטר מכתובת כמו /users/:id?

    1. props.id
    2. window.id
    3. useState('id')
    4. useParams()
    הצגת התשובה

    תשובה ד. useParams מחזיר את הפרמטרים מהנתיב.