"بطل تكتب فحوصات الـ auth جوا المكونات بتاعتك. beforeLoad ونمط _authenticated في TanStack Router بيديك حماية حقيقية للـ routes—على مستوى الـ routing نفسه، قبل ما أي Component يعمل Render أساسًا."
ترميز سعيد! — Ahmed Fahmy
معظم تطبيقات React بتحمي الـ routes كده:
// ❌ الطريقة السطحية
function Dashboard() {
const { user } = useAuth()
if (!user) {
return <Navigate to="/login" />
}
return <DashboardContent />
}
ده بيشتغل. بالكاد. بس فيه مشاكل حقيقية.
أول مشكلة إن الـ Component بيعمل Render قبل تنفيذ فحص الـ Auth، وده ممكن يسبب Flash بسيط يظهر فيه المحتوى المحمي للحظة.
ثاني مشكلة إنك تحتاج تكرر نفس منطق الحماية داخل كل Component محمي. ولو نسيت تضيفه مرة واحدة فقط، يصبح لديك Route غير محمي.
والمشكلة الأكبر؟
إن كل ده على مستوى الـ Component نفسه — بعد الـ Routing، وبعد بداية الـ Render، وحتى بعد تشغيل الـ Loader أحيانًا.
TanStack Router يحل ده كله بنقل فحوصات الـ Auth إلى المكان الصحيح: طبقة الـ Routing نفسها.قبل تحميل أي شيء.وقبل أن يعمل أي Component Render أساسًا.
TanStack Router بيديك أداتين لحماية الـ auth. الاتنين مختلفين وهتستخدم الاتنين.
beforeLoad — بيشتغل قبل الـ loader، قبل المكون. بيعمل redirect فوراً لو المستخدم مش مسجل دخول. مثالي لحماية route واحدة أو subtree.
_authenticated layout route — وهو Pathless Layout يلتف حول مجموعة Routes كاملة. بدل ما تكتب beforeLoad في كل Route، تكتبه مرة واحدة فقط داخل Layout وتحمي كل ما بداخله. ده النمط الصح لمعظم التطبيقات.
قبل ما تقدر تستخدم الـ auth في beforeLoad، محتاج تمرر حالة الـ auth بتاعتك من خلال route context. دي طريقة TanStack Router لإتاحة البيانات لكل route من غير prop drilling.
// src/main.tsx
import { createRouter, RouterProvider } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
import { useAuth } from './lib/auth'
const router = createRouter({
routeTree,
context: {
auth: undefined!, // هيتعبى تحت
},
})
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
function App() {
const auth = useAuth() // hook الـ auth بتاعك
return <RouterProvider router={router} context={{ auth }} />
}
دلوقتي عرّف شكل auth في الـ root route:
// src/routes/__root.tsx
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'
interface RouterContext {
auth: {
user: User | null
isLoading: boolean
}
}
export const Route = createRootRouteWithContext<RouterContext>()({
component: () => <Outlet />,
})
نداء createRootRouteWithContext ده هو اللي بيخلي TypeScript يفهم شكل الـ context في كل مكان في شجرة الـ routes.
ده النمط. اعمل pathless layout route بتحمي كل حاجة تحتها.
src/routes/
├── __root.tsx
├── index.tsx → / (عام)
├── (auth)/
│ ├── login.tsx → /login
│ └── register.tsx → /register
└── _authenticated/
├── _authenticated.tsx → layout + auth guard (من غير URL segment)
├── dashboard.tsx → /dashboard ✅ محمي
├── settings.tsx → /settings ✅ محمي
└── posts/
├── index.tsx → /posts ✅ محمي
└── $postId.tsx → /posts/:postId ✅ محمي
كل الـ routes تحت _authenticated/ محمية بحارس واحد. دي قوة الـ layout routes.
// src/routes/_authenticated/_authenticated.tsx
import { createFileRoute, redirect, Outlet } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated')({
beforeLoad: ({ context, location }) => {
if (!context.auth.user) {
throw redirect({
to: '/login',
search: {
redirect: location.href, // اتذكر كانوا رايحين فين
},
})
}
},
component: AuthenticatedLayout,
})
function AuthenticatedLayout() {
return (
<div>
<Sidebar />
<main>
<Outlet />
</main>
</div>
)
}
وده كل ما تحتاجه للحماية.إذا لم يكن هناك مستخدم مسجل دخول، يتم عمل Redirect إلى /login مع الاحتفاظ بالـ Destination URL داخل الـ Search Params.وأي Route موجود داخل _authenticated/ سيحصل على هذا الفحص تلقائيًا بدون تكرار أي Logic إضافي.
صفحة الـ login المفروض تعمل redirect للمستخدمين المسجلين دخول، وترجعهم للوجهة الأصلية بعد الـ login.
// src/routes/(auth)/login.tsx
import { createFileRoute, redirect, useRouter } from '@tanstack/react-router'
import { z } from 'zod'
import { fallback } from '@tanstack/zod-adapter'
const loginSearchSchema = z.object({
redirect: fallback(z.string(), '/dashboard').default('/dashboard'),
})
export const Route = createFileRoute('/(auth)/login')({
validateSearch: loginSearchSchema,
beforeLoad: ({ context, search }) => {
// مسجل دخول بالفعل؟ ابعته للمكان اللى كان رايحه
if (context.auth.user) {
throw redirect({ to: search.redirect })
}
},
component: LoginPage,
})
function LoginPage() {
const { redirect: redirectTo } = Route.useSearch()
const router = useRouter()
const { signIn } = Route.useRouteContext({ select: (ctx) => ctx.auth })
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
const formData = new FormData(e.currentTarget)
await signIn({
email: formData.get('email') as string,
password: formData.get('password') as string,
})
// تحديث حالة الـ Router لإعادة قراءة الـ Auth State الجديدة
router.invalidate()
// الرجوع للصفحة التي كان المستخدم يحاول الوصول إليها
router.navigate({ to: redirectTo })
}
return (
<form onSubmit={handleSubmit}>
<input type="email" name="email" placeholder="البريد الإلكتروني" required />
<input type="password" name="password" placeholder="كلمة المرور" required />
<button type="submit">تسجيل الدخول</button>
</form>
)
}
استدعاء router.invalidate() مهم جدًا هنا.لأنه يخبر TanStack Router بإعادة تشغيل جميع الـ Loaders وعمليات beforeLoad النشطة.وبما أن الـ Auth Context أصبح محدثًا بعد تسجيل الدخول، فإن فحص الحماية داخل _authenticated سيمر بنجاح هذه المرة.
مش كل الـ routes المحمية متساوية. بعض الصفحات تحتاج صلاحيات إضافية مثل Admin Access. وهنا أيضًا يأتي دورbeforeLoad.
// src/routes/_authenticated/admin.tsx
import { createFileRoute, redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated/admin')({
beforeLoad: ({ context }) => {
// _authenticated فحصت وجود المستخدم بالفعل
// هنا بنفحص الـ role
if (context.auth.user?.role !== 'admin') {
throw redirect({ to: '/dashboard' })
}
},
component: AdminPage,
})
function AdminPage() {
return <h1>لوحة الإدارة</h1>
}
تقدر أيضًا تعمل Stack لأكثر من beforeLoad guard بسهولة.
مثلًا:
الـ Layout الأب يتحقق من أن المستخدم مسجل دخول
والـ Child Route يتحقق من الـ Role أو الصلاحيات المطلوبة
وبهذا تحصل على فصل واضح ومنظم للمسؤوليات (Separation of Concerns) بدل وضع كل شيء في مكان واحد.
لو فحص الـ auth بتاعك async (بيتحقق من session مع السيرفر مثلاً)، ما تخليش الصفحة كلها واقفة. استخدم pendingComponent على الـ root route:
// src/routes/__root.tsx
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'
export const Route = createRootRouteWithContext<RouterContext>()({
component: () => <Outlet />,
pendingComponent: () => (
<div className="flex items-center justify-center h-screen">
<span>جاري التحميل...</span>
</div>
),
})
أو استخدم pendingMs عشان تورّي الـ spinner بس لو الموضوع اخد وقت أطول من حد معين:
const router = createRouter({
routeTree,
context: { auth: undefined! },
defaultPendingMs: 200, // ورّي الـ pending UI بس لو التحميل اخد > 200ms
defaultPendingMinMs: 500, // خليه يتعرض على الأقل 500ms عشان متبقش وميض
})
هنا مثال كامل يوضح كيف تربط كل الأجزاء معًا لبناء نظام Authentication متكامل.
src/
├── lib/
│ └── auth.ts → useAuth hook، getCurrentUser
├── routes/
│ ├── __root.tsx → root مع context
│ ├── index.tsx → /
│ ├── (auth)/
│ │ ├── login.tsx → /login
│ │ └── register.tsx → /register
│ └── _authenticated/
│ ├── _authenticated.tsx → auth guard
│ ├── dashboard.tsx → /dashboard
│ ├── settings.tsx → /settings
│ └── admin.tsx → /admin (فحص الـ role)
// src/lib/auth.ts
import { useState, useEffect } from 'react'
export interface User {
id: string
name: string
email: string
role: 'user' | 'admin'
}
export function useAuth() {
const [user, setUser] = useState<User | null>(null)
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
getCurrentUser()
.then(setUser)
.finally(() => setIsLoading(false))
}, [])
async function signIn(credentials: { email: string; password: string }) {
const user = await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify(credentials),
}).then(r => r.json())
setUser(user)
return user
}
async function signOut() {
await fetch('/api/auth/logout', { method: 'POST' })
setUser(null)
}
return { user, isLoading, signIn, signOut }
}
async function getCurrentUser(): Promise<User | null> {
try {
return await fetch('/api/auth/me').then(r => r.json())
} catch {
return null
}
}
// src/routes/_authenticated/_authenticated.tsx
import { createFileRoute, redirect, Outlet, Link } from '@tanstack/react-router'
import { useRouter } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated')({
beforeLoad: ({ context, location }) => {
if (context.auth.isLoading) return // لسه بيتحقق، ما تعملش redirect دلوقتي
if (!context.auth.user) {
throw redirect({
to: '/login',
search: { redirect: location.href },
})
}
},
component: AuthenticatedLayout,
})
function AuthenticatedLayout() {
const { user, signOut } = Route.useRouteContext({ select: (ctx) => ctx.auth })
const router = useRouter()
async function handleSignOut() {
await signOut()
router.invalidate()
router.navigate({ to: '/login' })
}
return (
<div className="flex h-screen">
<aside className="w-64 border-r p-4">
<nav className="flex flex-col gap-2">
<Link to="/dashboard" activeProps={{ className: 'font-bold' }}>
لوحة التحكم
</Link>
<Link to="/settings" activeProps={{ className: 'font-bold' }}>
الإعدادات
</Link>
{user?.role === 'admin' && (
<Link to="/admin" activeProps={{ className: 'font-bold' }}>
الإدارة
</Link>
)}
</nav>
<button onClick={handleSignOut} className="mt-auto">
تسجيل الخروج
</button>
</aside>
<main className="flex-1 p-6">
<Outlet />
</main>
</div>
)
}
النهج | Component Guard | Route Guard (beforeLoad) |
|---|---|---|
الفحص بيحصل | بعد ما العرض يبدأ | قبل ما الـ loader يشتغل |
وميض المحتوى المحمي | ممكن | لأ |
مكان منطق الـ auth | جوا المكون | في تعريف الـ route |
تطبيقه على N من الـ routes | نسخ ولصق N مرة | layout route واحد |
الـ redirect بيشيل الوجهة | يدوي | مدمج مع الـ search params |
أمان TypeScript | ضعيف | inference كامل للـ context |
استخدم createRootRouteWithContext: ما تمررش الـ auth كـ prop أبداً. الـ route context هو المكان الصحيح لمشاركة بيانات الـ Auth داخل التطبيق.
احتفظ بصفحة الـ Redirect الأصلية: دايماً مرر location.href في الـ search params لما بتعمل redirect للـ login. المستخدمين بيكرهوا لما يخسروا مكانهم.
استدعِ router.invalidate() بعد الـ login/logout: ده بيشغل beforeLoad في كل مكان من جديد. من غيره قد تعتمد الحماية على Auth State قديمة وغير محدثة.
ما توقفش على isLoading: لو حالة الـ auth بتتحمل، ارجع بدري من beforeLoad بدل ما تعمل redirect. خلي الـ pending UI يتعامل معاها.
افصل بين Authentication وRole Checks: استخدم Layout مخصص للتحقق من تسجيل الدخول، ثم أضف beforeLoad منفصل داخل الـ Routes التي تحتاج Roles أو Permissions معينة. خلط الاثنين معًا يجعل الـ Logic معقدًا ومربكًا لاحقًا.
فحوصات الـ auth على مستوى المكون دي لزقة جرح. بتشتغل، بس هشة، مكررة، ومتأخرة جداً—لأن عملية الـ Routing تكون قد حدثت بالفعل قبل تنفيذها.
beforeLoad في TanStack Router بينقل الحماية لمكانها الصح: طبقة الـ routing. عرّفها مرة واحدة في layout route، وكل child route بترثها تلقائياً. مفيش نسخ ولصق. مفيش نسيان Route غير محمي. مفيش وميض للمحتوى المحمي.
ده شكل حماية الـ routes الحقيقية.
المصادر: