React Router: التوجيه المتقدم
مقدمة إلى التوجيه المتقدم
يغطي هذا الدرس تقنيات React Router المتقدمة بما في ذلك التنقل البرمجي، واستخراج معاملات المسار، والمسارات المحمية، وعمليات إعادة التوجيه، وتقسيم الكود مع التحميل الكسول. هذه الأنماط ضرورية لبناء تطبيقات معقدة وجاهزة للإنتاج.
التنقل البرمجي مع useNavigate
يتيح لك خطاف useNavigate التنقل برمجيًا استجابةً للأحداث أو المنطق:
import { useNavigate } from 'react-router-dom';
function LoginForm() {
const navigate = useNavigate();
const [credentials, setCredentials] = useState({
email: '',
password: ''
});
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
if (response.ok) {
const user = await response.json();
localStorage.setItem('user', JSON.stringify(user));
// الانتقال إلى لوحة التحكم بعد تسجيل الدخول الناجح
navigate('/dashboard');
// الانتقال مع الحالة
// navigate('/dashboard', { state: { from: 'login' } });
// الانتقال واستبدال السجل (بدون زر الرجوع)
// navigate('/dashboard', { replace: true });
}
} catch (error) {
console.error('فشل تسجيل الدخول:', error);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={credentials.email}
onChange={(e) => setCredentials({ ...credentials, email: e.target.value })}
/>
<input
type="password"
value={credentials.password}
onChange={(e) => setCredentials({ ...credentials, password: e.target.value })}
/>
<button type="submit">تسجيل الدخول</button>
{/* التنقل البرمجي كبديل لـ Link */}
<button type="button" onClick={() => navigate(-1)}>العودة</button>
<button type="button" onClick={() => navigate(-2)}>العودة صفحتين</button>
</form>
);
}
نصيحة: استخدم navigate(-1) للعودة صفحة واحدة، navigate(-2) لصفحتين، إلخ. الأرقام الموجبة تتقدم في السجل.
useLocation: الوصول إلى الموقع الحالي
يوفر خطاف useLocation الوصول إلى كائن موقع URL الحالي:
import { useLocation, useNavigate } from 'react-router-dom';
function Dashboard() {
const location = useLocation();
const navigate = useNavigate();
// الوصول إلى الحالة الممررة أثناء التنقل
const fromPage = location.state?.from;
// الوصول إلى معاملات البحث
const searchParams = new URLSearchParams(location.search);
const tab = searchParams.get('tab') || 'overview';
console.log({
pathname: location.pathname, // '/dashboard'
search: location.search, // '?tab=profile'
hash: location.hash, // '#section1'
state: location.state, // { from: 'login' }
key: location.key // مفتاح فريد لهذا الموقع
});
return (
<div>
<h1>لوحة التحكم</h1>
{fromPage && <p>مرحبًا بك مرة أخرى من {fromPage}!</p>}
<div>
<button onClick={() => navigate('?tab=overview')}>نظرة عامة</button>
<button onClick={() => navigate('?tab=profile')}>الملف الشخصي</button>
<button onClick={() => navigate('?tab=settings')}>الإعدادات</button>
</div>
{tab === 'overview' && <Overview />}
{tab === 'profile' && <Profile />}
{tab === 'settings' && <Settings />}
</div>
);
}
// استخدام الموقع للعرض الشرطي
function Navigation() {
const location = useLocation();
const isActive = (path) => location.pathname === path;
return (
<nav>
<Link
to="/"
className={isActive('/') ? 'active' : ''}
>
الرئيسية
</Link>
<Link
to="/about"
className={isActive('/about') ? 'active' : ''}
>
عن
</Link>
</nav>
);
}
المسارات المحمية (المصادقة)
تنفيذ حماية المسار لتقييد الوصول للمستخدمين المصادق عليهم:
// src/components/ProtectedRoute.jsx
import { Navigate, useLocation } from 'react-router-dom';
function ProtectedRoute({ children }) {
const location = useLocation();
const isAuthenticated = checkAuth(); // وظيفة فحص المصادقة الخاصة بك
if (!isAuthenticated) {
// إعادة التوجيه إلى تسجيل الدخول، حفظ الموقع المحاول
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}
// مساعد فحص المصادقة
function checkAuth() {
const user = localStorage.getItem('user');
return !!user;
}
// src/App.jsx
import { Routes, Route } from 'react-router-dom';
import ProtectedRoute from './components/ProtectedRoute';
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
{/* مسار محمي واحد */}
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
{/* مسارات متداخلة محمية */}
<Route
path="/admin"
element={
<ProtectedRoute>
<AdminLayout />
</ProtectedRoute>
}
>
<Route index element={<AdminDashboard />} />
<Route path="users" element={<Users />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
);
}
ملاحظة: خاصية replace تمنع ظهور المسار المحمي في سجل المتصفح، لذلك لا يمكن للمستخدمين العودة إليه بعد تسجيل الخروج.
حماية المسار بناءً على الدور
قم بتوسيع المسارات المحمية للتحقق من أدوار المستخدم:
// src/components/RoleProtectedRoute.jsx
import { Navigate } from 'react-router-dom';
function RoleProtectedRoute({ children, allowedRoles }) {
const user = JSON.parse(localStorage.getItem('user') || '{}');
const isAuthenticated = !!user.id;
const hasRequiredRole = allowedRoles.includes(user.role);
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
if (!hasRequiredRole) {
return <Navigate to="/unauthorized" replace />;
}
return children;
}
// الاستخدام في App.jsx
function App() {
return (
<Routes>
{/* مسارات المسؤول فقط */}
<Route
path="/admin/*"
element={
<RoleProtectedRoute allowedRoles={['admin']}>
<AdminPanel />
</RoleProtectedRoute>
}
/>
{/* مسارات المحرر والمسؤول */}
<Route
path="/editor/*"
element={
<RoleProtectedRoute allowedRoles={['editor', 'admin']}>
<EditorPanel />
</RoleProtectedRoute>
}
/>
{/* أي مستخدم مصادق عليه */}
<Route
path="/profile"
element={
<RoleProtectedRoute allowedRoles={['user', 'editor', 'admin']}>
<Profile />
</RoleProtectedRoute>
}
/>
</Routes>
);
}
إعادة التوجيه ومكون Navigate
استخدم مكون Navigate لعمليات إعادة التوجيه التصريحية:
import { Routes, Route, Navigate } from 'react-router-dom';
function App() {
const isAuthenticated = checkAuth();
return (
<Routes>
{/* إعادة التوجيه من URL قديم إلى جديد */}
<Route path="/old-about" element={<Navigate to="/about" replace />} />
{/* إعادة توجيه شرطية */}
<Route
path="/login"
element={isAuthenticated ? <Navigate to="/dashboard" /> : <Login />}
/>
{/* إعادة التوجيه مع الحالة */}
<Route
path="/checkout"
element={
isAuthenticated
? <Checkout />
: <Navigate to="/login" state={{ returnTo: '/checkout' }} />
}
/>
{/* إعادة توجيه الجذر إلى صفحة محددة */}
<Route path="/" element={<Navigate to="/home" replace />} />
<Route path="/home" element={<Home />} />
</Routes>
);
}
تحذير: تجنب إنشاء حلقات إعادة التوجيه. تأكد دائمًا من أن منطق إعادة التوجيه له نقطة إنهاء واضحة.
التحميل الكسول للمسارات
قسّم كود مساراتك لتحسين الأداء باستخدام lazy و Suspense من React:
import { Routes, Route } from 'react-router-dom';
import { lazy, Suspense } from 'react';
// التحميل الفوري (افتراضي)
import Home from './pages/Home';
import About from './pages/About';
// التحميل الكسول - يُحمل فقط عند الوصول إلى المسار
const Dashboard = lazy(() => import('./pages/Dashboard'));
const AdminPanel = lazy(() => import('./pages/AdminPanel'));
const UserProfile = lazy(() => import('./pages/UserProfile'));
const Settings = lazy(() => import('./pages/Settings'));
// مكون التحميل
function LoadingSpinner() {
return (
<div className="loading">
<div className="spinner"></div>
<p>جاري التحميل...</p>
</div>
);
}
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
{/* مسارات محملة فوريًا */}
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
{/* مسارات محملة كسولًا */}
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/admin" element={<AdminPanel />} />
<Route path="/profile/:userId" element={<UserProfile />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
أفضل ممارسة: حمّل كسولًا المكونات الثقيلة (لوحات التحكم، لوحات المسؤول) وحمّل فوريًا المسارات الحرجة (الرئيسية، تسجيل الدخول) لوقت تحميل أولي مثالي.
معاملات البحث مع useSearchParams
إدارة معاملات الاستعلام باستخدام خطاف useSearchParams:
import { useSearchParams } from 'react-router-dom';
function ProductsList() {
const [searchParams, setSearchParams] = useSearchParams();
// قراءة معاملات البحث
const category = searchParams.get('category') || 'all';
const sort = searchParams.get('sort') || 'name';
const page = parseInt(searchParams.get('page') || '1', 10);
// تحديث معاملات البحث
const handleCategoryChange = (newCategory) => {
setSearchParams({
category: newCategory,
sort,
page: '1' // إعادة التعيين إلى الصفحة 1 عند تغيير المرشح
});
};
const handleSortChange = (newSort) => {
setSearchParams({
category,
sort: newSort,
page: String(page)
});
};
const handlePageChange = (newPage) => {
setSearchParams({
category,
sort,
page: String(newPage)
});
};
// حذف معامل
const clearFilters = () => {
setSearchParams({});
};
return (
<div>
<h1>المنتجات</h1>
{/* URL الحالي: /products?category=electronics&sort=price&page=2 */}
<div>
<select value={category} onChange={(e) => handleCategoryChange(e.target.value)}>
<option value="all">جميع الفئات</option>
<option value="electronics">الإلكترونيات</option>
<option value="clothing">الملابس</option>
</select>
<select value={sort} onChange={(e) => handleSortChange(e.target.value)}>
<option value="name">الاسم</option>
<option value="price">السعر</option>
<option value="date">التاريخ</option>
</select>
<button onClick={clearFilters}>مسح المرشحات</button>
</div>
{/* قائمة المنتجات بناءً على المرشحات */}
<ProductGrid category={category} sort={sort} page={page} />
{/* التصفح */}
<button onClick={() => handlePageChange(page - 1)} disabled={page === 1}>
السابق
</button>
<span>الصفحة {page}</span>
<button onClick={() => handlePageChange(page + 1)}>
التالي
</button>
</div>
);
}
تمرين 1: تدفق المصادقة
المهمة: تنفيذ تدفق مصادقة كامل:
- أنشئ صفحة تسجيل دخول تخزن كائن مستخدم وهمي في localStorage
- نفّذ مكون
ProtectedRoute - احمِ مسارات
/dashboardو/profile - أعد التوجيه إلى تسجيل الدخول إذا لم تتم المصادقة، مع حفظ المسار المحاول
- بعد تسجيل الدخول، أعد التوجيه إلى المسار المحاول أصلاً (أو لوحة التحكم)
- أضف زر تسجيل خروج يمسح localStorage ويعيد التوجيه إلى الرئيسية
تمرين 2: البحث المتقدم بمعاملات URL
المهمة: بناء صفحة بحث المنتجات بالمرشحات:
- استخدم
useSearchParamsلإدارة الفئة ونطاق السعر وترتيب الفرز - يجب أن تحدث جميع المرشحات سلسلة استعلام URL
- يجب أن تعمل الصفحة بشكل صحيح عند مشاركة URL (تستمر المرشحات)
- أضف زر "مسح جميع المرشحات"
- نفّذ التصفح بأرقام الصفحات في URL
تمرين 3: لوحة تحكم بالتحميل الكسول
المهمة: تحسين تطبيق لوحة تحكم بالتحميل الكسول:
- أنشئ مكونات منفصلة للوحة التحكم والتحليلات والتقارير والإعدادات
- حمّل كسولًا جميع المكونات المتعلقة بلوحة التحكم
- أنشئ مكون تحميل مخصص بواجهة مستخدم هيكلية
- حمّل فوريًا صفحات الرئيسية وتسجيل الدخول
- اختبر علامة التبويب network للتحقق من تقسيم الكود
الخلاصة
في هذا الدرس، أتقنت تقنيات React Router المتقدمة:
- التنقل البرمجي مع
useNavigateللتوجيه الديناميكي - الوصول إلى بيانات الموقع الحالي مع
useLocation - تطبيق المسارات المحمية بفحوصات المصادقة
- التحكم في الوصول بناءً على الدور لأنواع المستخدمين المختلفة
- عمليات إعادة التوجيه التصريحية مع مكون
Navigate - تقسيم الكود والتحميل الكسول لتحسين الأداء
- إدارة معاملات بحث URL مع
useSearchParams
في الدرس التالي، سنستكشف أساليب التنسيق المختلفة في React، بما في ذلك CSS Modules وstyled-components وTailwind CSS.