← السابق المستوى 4 - الدرس 4 من 12

🧭 React Router: بناء تطبيقات متعددة الصفحات بسلاسة

دليلك الشامل للتنقل بين الصفحات في تطبيقات React Single Page Application (SPA)

تقدمك في المستوى الرابع 33%

🎯 ما ستتعلمه في هذا الدرس

  • فهم React Router ولماذا نحتاجه في تطبيقات React
  • إعداد التوجيه الأساسي مع BrowserRouter و Routes
  • المسارات الديناميكية (Dynamic Routes) مع useParams
  • التنقل البرمجي باستخدام useNavigate
  • حماية المسارات (Protected Routes) للصفحات الخاصة
  • التحميل الكسول (Lazy Loading) لتحسين الأداء
  • تنظيم هيكل التطبيق في مشاريع حقيقية

📖 ما هو React Router ولماذا هو ضروري؟

React Router هي مكتبة قوية جداً للتنقل والملاحة في تطبيقات React. في تطبيقات الويب التقليدية (مثل PHP أو WordPress)، كل صفحة جديدة تتطلب تحميل كامل من السيرفر — HTML جديد، CSS جديد، JavaScript جديد. هذا يستغرق 1-3 ثوانٍ ويُرهق السيرفر.

React Router تسمح ببناء Single Page Applications (SPA) — تطبيقات صفحة واحدة تتغير محتوياتها ديناميكياً بدون إعادة تحميل الصفحة بالكامل. عندما تنتقل من "الرئيسية" إلى "من نحن"، React Router يبدل المكون المعروض فقط — بدون طلب جديد من السيرفر. هذا يجعل التطبيق أسرع بـ 10 أضعاف وأكثر سلاسة.

في مشروع لوحة تحكم (Admin Dashboard) عملته لشركة صغيرة، كان هناك 15 صفحة مختلفة. بدون React Router، كل انتقال كان يستغرق 2 ثانية. مع React Router، الانتقال أصبح فورياً — والعميل فرح جداً بالسرعة. هذا الفرق يُحدث!

لماذا لا نستخدم روابط HTML العادية (<a href>

الروابط العادية (<a href="/about">) تُسبب إعادة تحميل الصفحة كاملة — تفقد State، تُعيد تشغيل JavaScript، وتُرهق السيرفر. <Link> من React Router يغير URL في المتصفح بدون إعادة التحميل، ويُبدل المكون المعروض فقط.

⚡ التثبيت والإعداد الكامل

# 1. تثبيت react-router-dom (الإصدار 6+) npm install react-router-dom # 2. الاستيرادات الأساسية — كل ما تحتاجه في ملف واحد import { BrowserRouter, // المكون الأساسي — يُغلف التطبيق كاملاً Routes, // يحتوي على كل المسارات Route, ">مسار واحد (URL → Component) Link, // رابط بدون إعادة تحميل (بديل <a href>) NavLink, // Link مع className نشط تلقائياً useParams, // الحصول على معاملات URL الديناميكية useNavigate, // التنقل برمجياً (بعد تسجيل دخول مثلاً) useLocation, // معرفة المسار الحالي Outlet, // مكان عرض المسارات المتداخلة Navigate // إعادة توجيه مباشرة } from 'react-router-dom';
💡 نصيحة: من الإصدار 6 (2021)، React Router تغيرت بشكل كبير. Switch أصبح Routes، وcomponent أصبح element. تأكد من استخدام الإصدار 6+ — الإصدارات القديمة مختلفة جداً!

🎯 الأساسيات — إعداد التوجيه في تطبيق React

React Router تعتمد على مفهوم بسيط: كل URL path يرتبط بمكون (Component) معين. عندما يزور المستخدم /about، React Router يعرض مكون About. عندما يزور /users، يعرض مكون Users.

هيكل التطبيق الأساسي

// App.js — المكون الرئيسي يُغلف التطبيق بـ BrowserRouter import { BrowserRouter, Routes, Route, Link, NavLink } from 'react-router-dom'; import Home from './pages/Home'; import About from './pages/About'; import Users from './pages/Users'; import UserDetail from './pages/UserDetail'; import Dashboard from './pages/Dashboard'; import Login from './pages/Login'; import NotFound from './pages/NotFound'; function App() { return ( <BrowserRouter> // Navbar — يظهر في كل الصفحات <nav className="navbar"> <div className="nav-brand">🚀 تطبيقي</div> <div className="nav-links"> // NavLink يضيف className "active" تلقائياً للرابط النشط <NavLink to="/" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link' } end // "end" يمنع تفعيل الرابط في المسارات الفرعية > 🏠 الرئيسية </NavLink> <NavLink to="/about" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link' } > ℹ️ من نحن </NavLink> <NavLink to="/users" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link' } > 👥 المستخدمين </NavLink> <NavLink to="/dashboard" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link' } > 📊 لوحة التحكم </NavLink> </div> </nav> // Routes — يحتوي على كل المسارات المتاحة <main className="main-content"> <Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> <Route path="/users" element={<Users />} /> <Route path="/users/:id" element={<UserDetail />} /> <Route path="/login" element={<Login />} /> <Route path="/dashboard" element={<Dashboard />} /> <Route path="*" element={<NotFound />} /> </Routes> </main> </BrowserRouter> ); } export default App;
⚠️ خطأ شائع 80% من المبتدئين: يستخدمون <a href="/about"> بدلاً من <Link to="/about">. <a> يُسبب إعادة تحميل الصفحة كاملة — تفقد State ويُعيد تشغيل JavaScript. استخدم Link أو NavLink دائماً!

🔗 Dynamic Route Parameters — المسارات الديناميكية

أحياناً تريد مساراً يتقبل معاملات متغيرة من URL. مثلاً، لعرض تفاصيل مستخدم معين: /users/123 أو /users/456. React Router يتيح هذا باستخدام :parameter في المسار.

في مشروع متجر إلكتروني عملته، كان هناك 500 منتج. بدلاً من إنشاء 500 مسار منفصل، استخدمت مساراً ديناميكياً واحداً: /products/:productId. المكون ProductDetail يستقبل productId ويجلب بيانات المنتج من API.

استخدام useParams

import { useParams, useNavigate } from 'react-router-dom'; import { useState, useEffect } from 'react'; function UserDetail() { // useParams() يُرجع كائن يحتوي على كل المعاملات الديناميكية const { id } = useParams(); const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const navigate = useNavigate(); useEffect(() => { const fetchUser = async () => { try { const res = await fetch(`https://api.example.com/users/${id}`); if (!res.ok) throw new Error('المستخدم غير موجود'); const data = await res.json(); setUser(data); } catch (err) { setError(err.message); } finally { setLoading(false); } }; fetchUser(); }, [id]); // يعيد الجلب عند تغيير id if (loading) return <div className="spinner">جاري التحميل...</div>; if (error) return ( <div className="error-container"> <h2>❌ {error}</h2> <button onClick={() => navigate('/users')}> ← العودة للقائمة </button> </div> ); return ( <div className="user-detail"> <button onClick={() => navigate('/users')} className="back-btn"> ← العودة </button> <img src={user.avatar} alt={user.name} className="user-avatar" /> <h1>{user.name}</h1> <p>📧 {user.email}</p> <p>📱 {user.phone}</p> <p>🏢 {user.department}</p> <div className="user-stats"> <div className="stat"> <span className="stat-value">{user.projects}</span> <span className="stat-label">مشاريع</span> </div> <div className="stat"> <span className="stat-value">{user.tasks}</span> <span className="stat-label">مهام</span> </div> </div> </div> ); } // في Routes: <Route path="/users/:id" element={<UserDetail />} /> // يمكن الآن الوصول إلى /users/1, /users/2, /users/abc // يمكن أيضاً استخدام معاملات متعددة: /products/:category/:id
💡 نصيحة احترافية: دائماً استخدم useEffect مع [id] كـ dependency عند جلب بيانات ديناميكية. إذا نسيّت، البيانات لن تتحدث عند تغيير المستخدم — وهذا خطأ شائع جداً!

🔄 التنقل البرمجي — useNavigate

أحياناً تريد التنقل إلى صفحة أخرى برمجياً بعد حدث معين — بعد تسجيل الدخول الناجح، بعد حفظ البيانات، أو بعد حذف عنصر. useNavigate يسمح بهذا.

مثال: تسجيل دخول مع إعادة توجيه

import { useNavigate, useLocation } from 'react-router-dom'; import { useState } from 'react'; function LoginPage() { const navigate = useNavigate(); const location = useLocation(); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); const [isLoading, setIsLoading] = useState(false); // إذا كان المستخدم يحاول الوصول لصفحة محمية، نعيده إليها بعد الدخول const from = location.state?.from?.pathname || '/dashboard'; const handleLogin = async (e) => { e.preventDefault(); setError(''); setIsLoading(true); try { const response = await fetch('https://api.example.com/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }) }); const data = await response.json(); if (!response.ok) { throw new Error(data.message || 'بيانات الدخول غير صحيحة'); } // ✅ حفظ التوكن في localStorage localStorage.setItem('token', data.token); localStorage.setItem('user', JSON.stringify(data.user)); // ✅ التنقل إلى الصفحة المطلوبة (أو Dashboard) navigate(from, { replace: true }); // replace: true — يستبدل التاريخ بدلاً من إضافة صفحة جديدة // هذا يمنع المستخدم من الرجوع لصفحة تسجيل الدخول بزر Back } catch (err) { setError(err.message); } finally { setIsLoading(false); } }; return ( <div className="login-page"> <h1>🔐 تسجيل الدخول</h1> {error && <div className="error">{error}</div>} <form onSubmit={handleLogin}> <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="البريد الإلكتروني" required /> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="كلمة المرور" required /> <button type="submit" disabled={isLoading}> {isLoading ? 'جاري الدخول...' : 'دخول'} </button> </form> <p> ليس لديك حساب؟{' '} <Link to="/register">سجل الآن</Link> </p> </div> ); }

🔒 Protected Routes — حماية المسارات الخاصة

في التطبيقات الحقيقية، بعض الصفحات تحتاج تسجيل دخول — مثل لوحة التحكم، الإعدادات، أو صفحة الملف الشخصي. Protected Routes تتحقق من صلاحية المستخدم قبل عرض الصفحة.

في مشروع لوحة تحكم عملته، كان هناك 8 صفحات محمية. بدلاً من تكرار كود التحقق في كل صفحة، أنشأت مكون ProtectedRoute واحداً يُستخدم في كل المسارات المحمية.

// components/ProtectedRoute.js import { Navigate, useLocation } from 'react-router-dom'; function ProtectedRoute({ children }) { const location = useLocation(); const token = localStorage.getItem('token'); const user = localStorage.getItem('user'); // التحقق من وجود التوكن والمستخدم if (!token || !user) { // ❌ غير مسجل — إعادة توجيه لصفحة الدخول // state={{ from: location }} — حفظ المسار الأصلي للعودة إليه return <Navigate to="/login" state={{ from: location }} replace />; } // ✅ مسجل — عرض المحتوى المحمي return children; } // استخدام ProtectedRoute في App.js function App() { return ( <BrowserRouter> <Navbar /> <Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> <Route path="/login" element={<Login />} /> // ✅ مسارات محمية — تتطلب تسجيل دخول <Route path="/dashboard" element={ <ProtectedRoute> <Dashboard /> </ProtectedRoute> } /> <Route path="/profile" element={ <ProtectedRoute> <Profile /> </ProtectedRoute> } /> <Route path="/settings" element={ <ProtectedRoute> <Settings /> </ProtectedRoute> } /> <Route path="*" element={<NotFound />} /> </Routes> </BrowserRouter> ); }
🎯 نصيحة احترافية: في التطبيقات الحقيقية، لا تستخدم localStorage وحده للتحقق. تحقق من صلاحية التوكن مع السيرفر عند تحميل الصفحة. استخدم Context API أو Redux لتخزين حالة المستخدم بشكل مركزي.

🌳 Nested Routes — المسارات المتداخلة

أحياناً تريد صفحة رئيسية تحتوي على عدة أقسام فرعية. مثلاً، لوحة التحكم تحتوي على "الإحصائيات"، "المنتجات"، "الطلبات" — كلها داخل نفس التصميم (Sidebar + Header). Nested Routes تتيح هذا.

// Dashboard.js — الصفحة الرئيسية مع Outlet للمسارات الفرعية import { Outlet, NavLink } from 'react-router-dom'; function Dashboard() { return ( <div className="dashboard-layout"> <aside className="dashboard-sidebar"> <h3>لوحة التحكم</h3> <nav> <NavLink to="/dashboard" end>📊 الإحصائيات</NavLink> <NavLink to="/dashboard/products">📦 المنتجات</NavLink> <NavLink to="/dashboard/orders">🛒 الطلبات</NavLink> <NavLink to="/dashboard/users">👥 المستخدمين</NavLink> </nav> </aside> <main className="dashboard-content"> <Outlet /> // هنا يُعرض المكون الفرعي </main> </div> ); } // App.js — المسارات المتداخلة <Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>}> <Route index element={<DashboardStats />} /> // /dashboard <Route path="products" element={<ProductList />} /> // /dashboard/products <Route path="orders" element={<OrderList />} /> // /dashboard/orders <Route path="users" element={<UserList />} /> // /dashboard/users <Route path="products/:id" element={<ProductEdit />} /> // /dashboard/products/123 </Route>

⚡ Lazy Loading — التحميل الكسول لتحسين الأداء

في التطبيقات الكبيرة (20+ صفحة)، تحميل كل الصفحات دفعة واحدة يُبطئ التطبيق. Lazy Loading يحمّل الصفحة فقط عندما يحتاجها المستخدم — يقلل حجم التحميل الأولي بنسبة 70%!

// App.js — Lazy Loading للصفحات الكبيرة import { Suspense, lazy } from 'react'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; // ✅ Lazy Loading — يُحمّل الصفحة فقط عند الحاجة const Home = lazy(() => import('./pages/Home')); const About = lazy(() => import('./pages/About')); const Users = lazy(() => import('./pages/Users')); const UserDetail = lazy(() => import('./pages/UserDetail')); const Dashboard = lazy(() => import('./pages/Dashboard')); const Login = lazy(() => import('./pages/Login')); // Loading Component — يُعرض أثناء تحميل الصفحة function LoadingSpinner() { return ( <div className="loading-container"> <div className="spinner"></div> <p>جاري تحميل الصفحة...</p> </div> ); } function App() { return ( <BrowserRouter> <Navbar /> // Suspense يُعرض LoadingSpinner أثناء تحميل أي صفحة <Suspense fallback={<LoadingSpinner />}> <Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> <Route path="/users" element={<Users />} /> <Route path="/users/:id" element={<UserDetail />} /> <Route path="/login" element={<Login />} /> <Route path="*" element={<NotFound />} /> </Routes> </Suspense> </BrowserRouter> ); }
🎯 متى تستخدم Lazy Loading؟ في التطبيقات التي تحتوي على 10+ صفحات. لا تستخدمه في صفحات صغيرة (أقل من 5 صفحات) — التعقيد لا يستحق الفائدة. في مشروعي (15 صفحة)، Lazy Loading قلل حجم التحميل الأولي من 800KB إلى 250KB!

🏠 تنظيم هيكل التطبيق في مشاريع حقيقية

النمط الأفضل للتطبيقات الكبيرة هو تنظيم المسارات في ملف منفصل وحفظ الصفحات في مجلد pages (أو views).

// هيكل مشروع حقيقي (15 صفحة) src/ components/ ← مكونات صغيرة قابلة لإعادة الاستخدام Navbar/ Navbar.js Navbar.css ProtectedRoute/ ProtectedRoute.js LoadingSpinner/ LoadingSpinner.js pages/ ← صفحات كاملة (مكونات كبيرة) Home/ Home.js Home.css About/ About.js Users/ Users.js UserDetail.js Dashboard/ Dashboard.js DashboardStats.js ProductList.js OrderList.js Login/ Login.js NotFound/ NotFound.js App.js ← المسارات الرئيسية index.js

📊 مقارنة سريعة: Link vs useNavigate vs Navigate

الأداة الاستخدام مثال
Link روابط نصية/أزرار في JSX القائمة، روابط التنقل
NavLink روابط مع تنسيق نشط تلقائياً Navbar مع underline للصفحة الحالية
useNavigate التنقل برمجياً بعد حدث بعد تسجيل دخول، حذف عنصر
Navigate إعادة توجيه مباشرة في JSX ProtectedRoute، redirect

🚀 تمرين عملي: بناء تطبيق مدونة متكامل

قبل الانتقال للدرس التالي، جرب بناء تطبيق مدونة بسيط يستخدم كل ما تعلمته:

// التحدي: أنشئ تطبيق مدونة (Blog) يحتوي على: // 1. صفحة رئيسية تعرض قائمة المقالات (Home) // 2. صفحة "من نحن" (About) // 3. صفحة تفاصيل مقال ديناميكية /posts/:id (PostDetail) // 4. صفحة لوحة تحكم محمية /dashboard (Dashboard) // 5. صفحة تسجيل دخول /login (Login) // 6. صفحة 404 غير موجودة (NotFound) // المتطلبات: // - Navbar مع NavLink (تنسيق نشط للصفحة الحالية) // - Dynamic Route للمقالات مع useParams // - ProtectedRoute للوحة التحكم // - useNavigate بعد تسجيل الدخول // - Lazy Loading للصفحات الكبيرة // - Nested Routes في Dashboard: /dashboard/posts, /dashboard/comments
📝 تلميح: ابدأ بـ 3 صفحات فقط (Home, About, PostDetail). ثم أضف Login وProtectedRoute. ثم Nested Routes. لا تحاول كل شيء دفعة واحدة — اختبر كل خطوة!

📚 الخلاصة والخطوات التالية

في هذا الدرس تعلمت:

الدرس القادم: سنتعلم إدارة الحالة العالمية (State Management) باستخدام Context API و Redux Toolkit — كيف نُبقي بيانات المستخدم متاحة في كل المكونات بدون Props Drilling!

🎯 مهمة قبل الدرس القادم: أكمل تمرين المدونة. جرب إضافة Nested Routes في Dashboard (Posts, Comments). شارك الكود على واتساب 01229068356 وسنراجعه معك!