الكتابات/tutorial/2026/05
Tutorial7 مايو 2026·28 دقيقة

TanStack Router v1: التوجيه الآمن نوعياً المعتمد على الملفات لتطبيقات React في 2026

أتقن TanStack Router v1، الموجه الآمن نوعياً بالكامل لتطبيقات React. ابنِ تطبيقاً حقيقياً يحتوي على مسارات متداخلة، والتحقق من معاملات البحث، ومحملات البيانات، وواجهة الانتظار — مع استنتاج TypeScript شامل.

نضج TanStack Router ليصبح أكثر الموجهات صرامة من حيث الأمان النوعي في منظومة React. على عكس React Router، حيث تكون سلاسل المسارات مكتوبة كنصوص ومعاملات البحث مجرد فكرة لاحقة، يتعامل TanStack Router مع كل مسار وكل معامل وكل شكل لحالة البحث كمواطن من الدرجة الأولى في TypeScript. إذا أخطأت في كتابة مسار، أو مررت معاملاً بنوع خاطئ، أو نسيت معامل بحث مطلوب، فإن المترجم يوقفك قبل شحن الحزمة.

في هذا الدرس ستبني تطبيق لوحة تحكم صغير ولكنه واقعي — قائمة منتجات، وتفاصيل المنتج، والبحث المفلتر — باستخدام TanStack Router v1 مع التوجيه المعتمد على الملفات، ومحملات المسارات، ومخططات معاملات البحث، وواجهة الانتظار. بنهايته ستفهم النموذج الذهني بشكل كافٍ لإسقاط الموجه في قاعدة كود إنتاجية.

المتطلبات الأساسية

قبل البدء، تأكد من توفر:

  • Node.js 20 أو أحدث
  • معرفة عملية بـ React 18 أو 19 وخطافاتها
  • ارتياح مع TypeScript Generics على مستوى مبتدئ إلى متوسط
  • محرر كود بدعم قوي لـ TypeScript (VS Code أو WebStorm)
  • حوالي 30 دقيقة من الوقت المركز

لست بحاجة إلى خبرة سابقة مع TanStack Query أو Start أو DB — الموجه قابل للاستخدام بشكل كامل بمفرده.

ما الذي ستبنيه

لوحة تحكم من صفحتين تحتوي على:

  • صفحة قائمة منتجات في /products مع معاملات بحث URL آمنة نوعياً للتصفية
  • صفحة تفاصيل منتج في /products/$productId مع محمّل بيانات على مستوى المسار
  • تخطيط متداخل يشترك في الشريط الجانبي عبر جميع مسارات لوحة التحكم
  • واجهة انتظار مدفوعة بخطافات على غرار useNavigation لتغذية راجعة فورية أثناء الانتقالات
  • مكونات Link مستنتجة بالكامل — لا توجد إمكانية لأخطاء إملائية في النصوص

كل شيء يعمل محلياً على Vite. لا حاجة لخادم خلفي.

الخطوة 1: إعداد المشروع

أنشئ مشروع Vite React TypeScript جديد وثبّت حزم الموجه.

npm create vite@latest tanstack-router-demo -- --template react-ts
cd tanstack-router-demo
npm install
npm install @tanstack/react-router
npm install -D @tanstack/router-plugin @tanstack/router-devtools

الحزم الثلاث المهمة:

  • @tanstack/react-router — الموجه أثناء التشغيل
  • @tanstack/router-plugin — مكون Vite الإضافي الذي يولّد شجرة المسارات من بنية ملفاتك
  • @tanstack/router-devtools — فاحص داخل المتصفح للمسارات النشطة والمحملات والمعاملات المطابقة

افتح vite.config.ts ووصّل المكون الإضافي. يجب تشغيل المكون الإضافي قبل مكون React حتى يحدث توليد المسار أولاً.

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
 
export default defineConfig({
  plugins: [
    TanStackRouterVite({ target: "react", autoCodeSplitting: true }),
    react(),
  ],
});

autoCodeSplitting: true هو الإعداد الافتراضي الموصى به في v1 — فهو يقسم كل ملف مسار كسولاً إلى جزء خاص به دون أن تكتب استيراداً ديناميكياً واحداً.

الخطوة 2: تعريف المسار الجذر

يستخدم TanStack Router تخطيط ملفات يقوم على الاتفاقيات تحت src/routes/. أنشئ هذا المجلد وأضف نقطة الدخول.

mkdir -p src/routes

أنشئ src/routes/__root.tsx. بادئة الشرطتين السفليتين تشير إلى أن الملف هو المسار الجذر — كل مسار آخر في الشجرة يتداخل بداخله.

import { createRootRoute, Link, Outlet } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/router-devtools";
 
export const Route = createRootRoute({
  component: RootLayout,
});
 
function RootLayout() {
  return (
    <div className="app">
      <header>
        <nav>
          <Link to="/" activeProps={{ className: "active" }}>
            الرئيسية
          </Link>
          <Link to="/products" activeProps={{ className: "active" }}>
            المنتجات
          </Link>
        </nav>
      </header>
      <main>
        <Outlet />
      </main>
      <TanStackRouterDevtools position="bottom-right" />
    </div>
  );
}

Outlet هو المكان الذي تُعرض فيه المسارات الفرعية. أدوات التطوير تُحمَّل مرة واحدة في الجذر وتظهر كزر عائم في الزاوية.

الخطوة 3: إضافة مسار الفهرس

أنشئ src/routes/index.tsx لصفحة الرئيسية.

import { createFileRoute } from "@tanstack/react-router";
 
export const Route = createFileRoute("/")({
  component: HomePage,
});
 
function HomePage() {
  return (
    <section>
      <h1>لوحة التحكم</h1>
      <p>مرحباً. قم بزيارة صفحة المنتجات للبدء.</p>
    </section>
  );
}

لاحظ المسار النصي الحرفي الذي يتم تمريره إلى createFileRoute("/"). يجب أن يطابق مسار الملف وإلا فإن البناء سيفشل — المكون الإضافي يفرض ذلك. هذا الحرفي أيضاً هو ما يجعل Link to="/" آمناً نوعياً: المسارات التي رآها المكون الإضافي فقط هي المسارات الصالحة.

الخطوة 4: تشغيل الموجه

الآن وصّل شجرة المسارات المولّدة في نقطة دخول React. استبدل src/main.tsx:

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { createRouter, RouterProvider } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen";
 
const router = createRouter({ routeTree });
 
declare module "@tanstack/react-router" {
  interface Register {
    router: typeof router;
  }
}
 
createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <RouterProvider router={router} />
  </StrictMode>,
);

تفصيلان مهمان:

ملف routeTree.gen.ts يُولَّد تلقائياً بواسطة مكون Vite الإضافي عند أول بناء أو تشغيل تطوير. إذا أبلغ محررك أنه مفقود، شغّل npm run dev مرة واحدة وسيظهر.

كتلة declare module تُسجّل نسخة الموجه الخاصة بك في نظام أنواع TanStack Router. بعد ذلك، تعرف الخطافات مثل useNavigate وuseParams كل مسار في تطبيقك، معاملاً بمعامل.

شغّل خادم التطوير وتأكد من تحميل الصفحة الرئيسية.

npm run dev

الخطوة 5: بناء قائمة المنتجات مع معاملات البحث

هذا هو المكان الذي يتفوق فيه TanStack Router على المنافسين. معاملات بحث URL مكتوبة نوعياً ومُتحقَّق منها ومُحلَّلة تلقائياً.

أنشئ src/routes/products.tsx. لاحظ أن هذا مسار تخطيط — له أبناء — لذا نستخدم مساراً غير ورقي.

import { createFileRoute, Outlet } from "@tanstack/react-router";
import { z } from "zod";
 
const productSearchSchema = z.object({
  category: z.enum(["all", "books", "electronics", "clothing"]).catch("all"),
  page: z.number().int().positive().catch(1),
  sort: z.enum(["name", "price"]).catch("name"),
});
 
export const Route = createFileRoute("/products")({
  validateSearch: productSearchSchema,
  component: ProductsLayout,
});
 
function ProductsLayout() {
  return (
    <div className="products-layout">
      <aside>
        <h2>المرشحات</h2>
      </aside>
      <Outlet />
    </div>
  );
}

ثبّت Zod إذا لم تكن قد فعلت ذلك بالفعل.

npm install zod

validateSearch يعمل عند كل عملية تنقل. استدعاءات .catch() توفر بدائل للمعاملات المفقودة أو المشوهة بدلاً من الرمي بخطأ — تفصيل صغير يوفر وقتاً هائلاً في تصحيح الأخطاء بمجرد دخول تطبيقك للإنتاج.

الآن أنشئ فهرس قسم المنتجات: src/routes/products.index.tsx. صيغة النقطة في اسم الملف تعني "مسار الفهرس المتداخل داخل /products".

import { createFileRoute, Link } from "@tanstack/react-router";
 
const allProducts = [
  { id: "p1", name: "المبرمج البراغماتي", category: "books", price: 32 },
  { id: "p2", name: "لوحة مفاتيح ميكانيكية", category: "electronics", price: 145 },
  { id: "p3", name: "هودي", category: "clothing", price: 65 },
  { id: "p4", name: "تصميم تطبيقات كثيفة البيانات", category: "books", price: 48 },
];
 
export const Route = createFileRoute("/products/")({
  component: ProductsList,
});
 
function ProductsList() {
  const { category, page, sort } = Route.useSearch();
 
  const filtered = allProducts
    .filter((p) => category === "all" || p.category === category)
    .sort((a, b) => (sort === "price" ? a.price - b.price : a.name.localeCompare(b.name)));
 
  return (
    <section>
      <header>
        <h1>المنتجات</h1>
        <nav className="filter-bar">
          <Link from={Route.fullPath} search={(prev) => ({ ...prev, category: "all" })}>
            الكل
          </Link>
          <Link from={Route.fullPath} search={(prev) => ({ ...prev, category: "books" })}>
            كتب
          </Link>
          <Link from={Route.fullPath} search={(prev) => ({ ...prev, category: "electronics" })}>
            إلكترونيات
          </Link>
        </nav>
        <p>
          عرض الصفحة {page}، مرتبة حسب {sort}
        </p>
      </header>
      <ul>
        {filtered.map((p) => (
          <li key={p.id}>
            <Link to="/products/$productId" params={{ productId: p.id }}>
              {p.name} — ${p.price}
            </Link>
          </li>
        ))}
      </ul>
    </section>
  );
}

نمطان يستحقان التنبيه:

Route.useSearch() يُرجع كائناً مكتوباً نوعياً بالكامل مشتقاً من مخطط Zod. لا يوجد تحليل يدوي. لا يوجد وصول مفتاحي نصي. إذا أعدت تسمية معامل بحث، فإن المترجم يشير إلى كل مستهلك له.

الصيغة الدالة search={(prev) => ({ ...prev, category: "books" })} تتيح لك تحديث معامل بحث واحد مع الحفاظ على الآخرين. أنظف من تجميع سلاسل الاستعلام يدوياً.

الخطوة 6: إضافة مسار تفاصيل المنتج مع محمّل

أنشئ src/routes/products.$productId.tsx. يشير الجزء $productId إلى معامل ديناميكي.

import { createFileRoute, Link, notFound } from "@tanstack/react-router";
 
type Product = { id: string; name: string; category: string; price: number };
 
const productDb: Record<string, Product> = {
  p1: { id: "p1", name: "المبرمج البراغماتي", category: "books", price: 32 },
  p2: { id: "p2", name: "لوحة مفاتيح ميكانيكية", category: "electronics", price: 145 },
  p3: { id: "p3", name: "هودي", category: "clothing", price: 65 },
  p4: { id: "p4", name: "تصميم تطبيقات كثيفة البيانات", category: "books", price: 48 },
};
 
export const Route = createFileRoute("/products/$productId")({
  loader: async ({ params }) => {
    await new Promise((r) => setTimeout(r, 300));
    const product = productDb[params.productId];
    if (!product) throw notFound();
    return { product };
  },
  pendingComponent: () => <p>جارٍ تحميل المنتج…</p>,
  notFoundComponent: () => <p>هذا المنتج غير موجود.</p>,
  component: ProductDetail,
});
 
function ProductDetail() {
  const { product } = Route.useLoaderData();
 
  return (
    <article>
      <Link to="/products" search={{ category: "all", page: 1, sort: "name" }}>
رجوع
      </Link>
      <h1>{product.name}</h1>
      <p>الفئة: {product.category}</p>
      <p>السعر: ${product.price}</p>
    </article>
  );
}

المحمّل loader يعمل قبل أن يُرسم المكون. أثناء تنفيذه، يُعرض pendingComponent تلقائياً — لا حاجة إلى علم تحميل يدوي. إذا رمى المحمّل notFound()، يتولى notFoundComponent المهمة. هذا هو نفس النموذج القابل للتركيب الذي اشتهر به Remix، ولكن مع استنتاج TypeScript الشامل على Route.useLoaderData().

الخطوة 7: واجهة الانتظار في عمليات التنقل البطيئة

افتراضياً، ينتظر TanStack Router حل المحملات قبل تبديل العرض، مما قد يبدو متأخراً. اضبط هذا باستخدام defaultPendingMs وdefaultPreload على الموجه نفسه. حدّث src/main.tsx:

const router = createRouter({
  routeTree,
  defaultPreload: "intent",
  defaultPreloadStaleTime: 30_000,
  defaultPendingMs: 200,
  defaultPendingMinMs: 500,
});

ماذا يفعل كل منها:

  • defaultPreload: "intent" يبدأ التحميل عند تمرير المؤشر فوق الرابط أو التركيز عليه، بحيث تكون البيانات جاهزة في الغالب بحلول نقرة المستخدم الفعلية
  • defaultPreloadStaleTime يمنع التحميلات المسبقة المتكررة خلال 30 ثانية
  • defaultPendingMs ينتظر 200 مللي ثانية قبل عرض واجهة الانتظار — التحميلات السريعة لا تومض المؤشر أبداً
  • defaultPendingMinMs يضمن بقاء المؤشر لمدة لا تقل عن 500 مللي ثانية بمجرد عرضه، مما يمنع الوميض

هذه الأسطر الأربعة هي الفرق بين تطبيق يبدو بطيئاً وتطبيق يبدو وكأنه يقرأ أفكار المستخدم.

الخطوة 8: التنقل البرمجي

داخل أي مكون يمكنك استخدام useNavigate للتنقل الأمري. الخطاف مكتوب بالكامل ضد شجرة المسارات.

import { useNavigate } from "@tanstack/react-router";
 
function CheckoutButton({ productId }: { productId: string }) {
  const navigate = useNavigate();
 
  return (
    <button
      onClick={() =>
        navigate({
          to: "/products/$productId",
          params: { productId },
        })
      }
    >
      اشترِ الآن
    </button>
  );
}

جرب إعادة تسمية $productId إلى $slug في ملف المسار. مسار خطأ TypeScript يقودك إلى كل استدعاء تنقل يحتاج إلى تحديث — تصبح إعادة الهيكلة آلية بدلاً من محفوفة بالمخاطر.

اختبار التنفيذ

اتبع قائمة التحقق هذه في المتصفح:

  • افتح /. تُعرض الصفحة الرئيسية مع شريط التنقل.
  • انقر على المنتجات. تظهر القائمة مع أزرار التصفية.
  • انقر على كتب. يصبح الـ URL هو /products?category=books&page=1&sort=name وتُصفّى القائمة.
  • عدّل الـ URL يدوياً إلى /products?category=spaceships. تُعرض الصفحة بـ category: "all" لأن المخطط رجع إلى البديل بسلاسة.
  • انقر على منتج. لاحظ حالة الانتظار القصيرة، ثم صفحة التفاصيل.
  • جرب /products/nonexistent مباشرة. يُعرض مكون "غير موجود".
  • افتح لوحة أدوات التطوير (أسفل اليمين) وتفقّد المسارات المطابقة وبيانات المحمّل ومعاملات البحث مباشرة.

إذا نجحت الاختبارات الثمانية، فالموجه موصول بشكل صحيح.

استكشاف الأخطاء وإصلاحها

ملف routeTree.gen.ts مفقود. شغّل خادم التطوير مرة واحدة. مكون Vite الإضافي يولّده عند التشغيل الأول ويعيد كتابته في كل حفظ ملف داخل src/routes/.

TypeScript يعتقد أن كل مسار من نوع string. نسيت كتلة declare module في main.tsx. بدونها، نظام الأنواع لا يعرف أن موجهك موجود.

Link to="/foo" لا يكمل تلقائياً. ربما فشل التقاط ملف المسار. تحقق من أن الملف موجود تحت src/routes/، يصدّر Route، وأن خادم التطوير قد أُعيد تشغيله مرة واحدة على الأقل.

معاملات البحث تُعاد إلى وضع البداية باستمرار. استخدمت صيغة الكائن search={{ category: "books" }} بدلاً من صيغة الدالة search={(prev) => ({ ...prev, category: "books" })}. صيغة الكائن تستبدل جميع المعاملات؛ صيغة الدالة تدمج.

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

الخاتمة

TanStack Router يستبدل التوجيه المكتوب نصياً بشجرة مفحوصة من قبل المترجم. عدم تطابق المسارات والمعاملات المفقودة وحالات البحث المشوهة كلها تصبح أخطاء بناء بدلاً من أخطاء وقت التشغيل. اجمع ذلك مع واجهة الانتظار المضمّنة، والتحميل المسبق المعتمد على النية، والتحقق من البحث المدعوم بـ Zod، وستحصل على موجه يجعل تطبيقات React تشعر بأنها أسرع حقاً — في الشحن والاستخدام معاً.

لوحة التحكم التي بنيتها للتو صغيرة، لكن الأنماط قابلة للتوسع. التخطيطات المتداخلة تتركّب. مخططات البحث تتركّب. المحملات تتركّب. ضع هذا الموجه في تطبيق React المتوسط القادم ولن تفتقد البدائل.