بناء تطبيق full-stack مع TanStack Start: إطار عمل React من الجيل القادم

AI Bot
بواسطة AI Bot ·

جاري تحميل مشغل تحويل النص إلى كلام الصوتي...

TanStack Start هو إطار عمل React الوصفي الجديد الذي يُحدث ثورة في النظام البيئي. مبني على TanStack Router وVite، يقدم توجيهاً type-safe، ودوال خادم، وSSR متدفق، ونشراً شاملاً — كل ذلك مع تجربة مطور استثنائية. في هذا الدليل، ستبني تطبيقاً full-stack من الصفر.

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

تطبيق TaskBoard — مدير مهام تعاوني يتضمن:

  • توجيه ملفات type-safe مع TanStack Router
  • دوال خادم لعمليات CRUD
  • SSR مع التدفق
  • التحقق من المدخلات مع Zod
  • برمجيات وسيطة للمصادقة
  • تخزين البيانات مع Prisma وSQLite
  • واجهة متجاوبة مع Tailwind CSS
  • النشر على Vercel

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

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

  • Node.js 20+ مُثبّت
  • npm أو pnpm كمدير حزم
  • معرفة أساسية بـ React وTypeScript
  • محرر أكواد (يُنصح بـ VS Code)
  • مفاهيم أساسية عن واجهات REST API

لماذا TanStack Start؟

قبل الغوص في الكود، دعنا نفهم لماذا يتميز TanStack Start في 2026:

الخاصيةTanStack StartNext.jsRemix
التوجيهType-safe، ملفاتملفاتملفات
محرك البناءViteWebpack/TurbopackVite
دوال الخادمcreateServerFnServer Actionsaction/loader
SSR المتدفقنعمنعمنعم
ISRنعمنعملا
وضع SPAنعملالا
Type-safetyشاملجزئيجزئي

يتبنى TanStack Start فلسفة client-first: تكتب React عادي، ويتولى الإطار إدارة تعقيدات الخادم بشكل شفاف.

الخطوة 1: تهيئة المشروع

ابدأ بإنشاء مشروع TanStack Start جديد من القالب الرسمي:

npx gitpick TanStack/router/tree/main/examples/react/start-basic taskboard
cd taskboard
npm install

تحقق من أن كل شيء يعمل:

npm run dev

يجب أن يكون تطبيقك متاحاً على http://localhost:3000.

هيكل المشروع

إليك الهيكل الأساسي الذي سنستخدمه:

taskboard/
├── app/
│   ├── routes/
│   │   ├── __root.tsx          # Layout racine
│   │   ├── index.tsx           # Page d'accueil
│   │   ├── tasks.tsx           # Liste des tâches
│   │   ├── tasks.$taskId.tsx   # Détail d'une tâche
│   │   └── api/
│   │       └── tasks.ts        # Route API
│   ├── components/
│   │   ├── TaskCard.tsx
│   │   ├── TaskForm.tsx
│   │   └── Header.tsx
│   ├── lib/
│   │   ├── db.ts               # Client Prisma
│   │   └── server-fns.ts       # Fonctions serveur
│   ├── client.tsx              # Point d'entrée client
│   ├── router.tsx              # Configuration du routeur
│   └── ssr.tsx                 # Point d'entrée SSR
├── prisma/
│   └── schema.prisma
├── app.config.ts               # Configuration TanStack Start
├── tailwind.config.ts
└── package.json

الخطوة 2: إعداد TanStack Start

ملف app.config.ts هو جوهر الإعدادات:

// app.config.ts
import { defineConfig } from "@tanstack/react-start/config";
import tailwindcss from "@tailwindcss/vite";
 
export default defineConfig({
  vite: {
    plugins: [tailwindcss()],
  },
});

هذا الإعداد المبسط يعتمد على Vite تحت الغطاء، مما يعني أوقات تشغيل فائقة السرعة وHMR (Hot Module Replacement) فوري.

الخطوة 3: إعداد قاعدة البيانات مع Prisma

ثبّت Prisma وهيّئه مع SQLite للبساطة:

npm install prisma @prisma/client
npx prisma init --datasource-provider sqlite

عرّف مخطط البيانات:

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}
 
datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}
 
model Task {
  id          String   @id @default(cuid())
  title       String
  description String?
  status      String   @default("todo") // todo, in_progress, done
  priority    String   @default("medium") // low, medium, high
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  authorId    String
  author      User     @relation(fields: [authorId], references: [id])
}
 
model User {
  id        String   @id @default(cuid())
  name      String
  email     String   @unique
  tasks     Task[]
  createdAt DateTime @default(now())
}

أنشئ ملف .env:

DATABASE_URL="file:./dev.db"

طبّق المخطط وولّد العميل:

npx prisma db push
npx prisma generate

أنشئ عميل Prisma القابل لإعادة الاستخدام:

// app/lib/db.ts
import { PrismaClient } from "@prisma/client";
 
const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined;
};
 
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
 
if (process.env.NODE_ENV !== "production") {
  globalForPrisma.prisma = prisma;
}

الخطوة 4: فهم دوال الخادم

دوال الخادم هي المفهوم الجوهري في TanStack Start. تتيح لك تنفيذ كود الخادم بشكل type-safe من أي مكان في تطبيقك.

مبدأ العمل

[العميل] → استدعاء الدالة → [الشبكة (RPC)] → [الخادم] → النتيجة → [العميل]

يستبدل البناء تلقائياً كود الخادم بـ stubs RPC على جانب العميل. لا يغادر كود الخادم الخادم أبداً.

الصيغة الأساسية

import { createServerFn } from "@tanstack/react-start";
 
const maFonctionServeur = createServerFn({ method: "GET" })
  .validator((data: unknown) => {
    // Validation des entrées
    return data as { id: string };
  })
  .handler(async ({ data }) => {
    // Ce code s'exécute uniquement sur le serveur
    return { result: "données du serveur" };
  });

إنشاء دوال خادم TaskBoard

// app/lib/server-fns.ts
import { createServerFn } from "@tanstack/react-start";
import { z } from "zod";
import { prisma } from "./db";
 
// Schémas de validation
const createTaskSchema = z.object({
  title: z.string().min(1, "Le titre est requis").max(200),
  description: z.string().optional(),
  priority: z.enum(["low", "medium", "high"]),
  authorId: z.string(),
});
 
const updateTaskSchema = z.object({
  id: z.string(),
  title: z.string().min(1).max(200).optional(),
  description: z.string().optional(),
  status: z.enum(["todo", "in_progress", "done"]).optional(),
  priority: z.enum(["low", "medium", "high"]).optional(),
});
 
// Récupérer toutes les tâches
export const getTasks = createServerFn({ method: "GET" }).handler(async () => {
  const tasks = await prisma.task.findMany({
    include: { author: true },
    orderBy: { createdAt: "desc" },
  });
  return tasks;
});
 
// Récupérer une tâche par ID
export const getTaskById = createServerFn({ method: "GET" })
  .validator((data: unknown) => {
    const parsed = z.object({ id: z.string() }).parse(data);
    return parsed;
  })
  .handler(async ({ data }) => {
    const task = await prisma.task.findUnique({
      where: { id: data.id },
      include: { author: true },
    });
 
    if (!task) {
      throw new Error("Tâche introuvable");
    }
 
    return task;
  });
 
// Créer une nouvelle tâche
export const createTask = createServerFn({ method: "POST" })
  .validator((data: unknown) => createTaskSchema.parse(data))
  .handler(async ({ data }) => {
    const task = await prisma.task.create({
      data: {
        title: data.title,
        description: data.description,
        priority: data.priority,
        authorId: data.authorId,
      },
      include: { author: true },
    });
    return task;
  });
 
// Mettre à jour une tâche
export const updateTask = createServerFn({ method: "POST" })
  .validator((data: unknown) => updateTaskSchema.parse(data))
  .handler(async ({ data }) => {
    const { id, ...updateData } = data;
    const task = await prisma.task.update({
      where: { id },
      data: updateData,
      include: { author: true },
    });
    return task;
  });
 
// Supprimer une tâche
export const deleteTask = createServerFn({ method: "POST" })
  .validator((data: unknown) => {
    return z.object({ id: z.string() }).parse(data);
  })
  .handler(async ({ data }) => {
    await prisma.task.delete({ where: { id: data.id } });
    return { success: true };
  });

نقطة مهمة: طريقة GET تتيح التخزين المؤقت وإلغاء تكرار الطلبات تلقائياً. استخدم POST للتغييرات (إنشاء، تحديث، حذف).

الخطوة 5: إعداد التوجيه المبني على الملفات

يستخدم TanStack Start نظام توجيه مبني على الملفات مع ميزة رئيسية: إنه type-safe بالكامل.

التخطيط الجذري

// app/routes/__root.tsx
import {
  Outlet,
  createRootRoute,
  HeadContent,
  Scripts,
} from "@tanstack/react-router";
import Header from "../components/Header";
 
export const Route = createRootRoute({
  head: () => ({
    meta: [
      { charSet: "utf-8" },
      { name: "viewport", content: "width=device-width, initial-scale=1" },
      { title: "TaskBoard — Gestionnaire de tâches" },
    ],
    links: [
      {
        rel: "stylesheet",
        href: "/src/app.css",
      },
    ],
  }),
  component: RootComponent,
});
 
function RootComponent() {
  return (
    <html lang="fr">
      <head>
        <HeadContent />
      </head>
      <body className="bg-gray-50 text-gray-900 min-h-screen">
        <Header />
        <main className="container mx-auto px-4 py-8">
          <Outlet />
        </main>
        <Scripts />
      </body>
    </html>
  );
}

الصفحة الرئيسية

// app/routes/index.tsx
import { createFileRoute, Link } from "@tanstack/react-router";
import { getTasks } from "../lib/server-fns";
 
export const Route = createFileRoute("/")({
  loader: async () => {
    const tasks = await getTasks();
    return { tasks };
  },
  component: HomePage,
});
 
function HomePage() {
  const { tasks } = Route.useLoaderData();
 
  const todoCount = tasks.filter((t) => t.status === "todo").length;
  const inProgressCount = tasks.filter(
    (t) => t.status === "in_progress"
  ).length;
  const doneCount = tasks.filter((t) => t.status === "done").length;
 
  return (
    <div>
      <div className="mb-8">
        <h1 className="text-3xl font-bold mb-2">Tableau de bord</h1>
        <p className="text-gray-600">
          Gérez vos tâches efficacement avec TaskBoard.
        </p>
      </div>
 
      {/* Statistiques */}
      <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
        <StatCard label="À faire" count={todoCount} color="bg-yellow-100 text-yellow-800" />
        <StatCard label="En cours" count={inProgressCount} color="bg-blue-100 text-blue-800" />
        <StatCard label="Terminées" count={doneCount} color="bg-green-100 text-green-800" />
      </div>
 
      {/* Actions */}
      <div className="flex gap-4">
        <Link
          to="/tasks"
          className="px-6 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors font-medium"
        >
          Voir toutes les tâches
        </Link>
      </div>
    </div>
  );
}
 
function StatCard({
  label,
  count,
  color,
}: {
  label: string;
  count: number;
  color: string;
}) {
  return (
    <div className={`rounded-xl p-6 ${color}`}>
      <p className="text-sm font-medium opacity-80">{label}</p>
      <p className="text-3xl font-bold mt-1">{count}</p>
    </div>
  );
}

لاحظ كيف أن Route.useLoaderData() مُنمّط بالكامل — يعرف TypeScript البنية الدقيقة للبيانات المُرجعة من الـ loader.

صفحة المهام

// app/routes/tasks.tsx
import { createFileRoute, Link, useRouter } from "@tanstack/react-router";
import { getTasks, updateTask, deleteTask } from "../lib/server-fns";
import TaskCard from "../components/TaskCard";
 
export const Route = createFileRoute("/tasks")({
  loader: async () => {
    const tasks = await getTasks();
    return { tasks };
  },
  component: TasksPage,
});
 
function TasksPage() {
  const { tasks } = Route.useLoaderData();
  const router = useRouter();
 
  const handleStatusChange = async (id: string, status: string) => {
    await updateTask({ data: { id, status: status as "todo" | "in_progress" | "done" } });
    router.invalidate();
  };
 
  const handleDelete = async (id: string) => {
    if (confirm("Supprimer cette tâche ?")) {
      await deleteTask({ data: { id } });
      router.invalidate();
    }
  };
 
  const columns = [
    { key: "todo", label: "À faire", color: "border-yellow-400" },
    { key: "in_progress", label: "En cours", color: "border-blue-400" },
    { key: "done", label: "Terminé", color: "border-green-400" },
  ];
 
  return (
    <div>
      <div className="flex justify-between items-center mb-8">
        <h1 className="text-3xl font-bold">Tâches</h1>
        <Link
          to="/tasks/new"
          className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors"
        >
          + Nouvelle tâche
        </Link>
      </div>
 
      {/* Vue Kanban */}
      <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
        {columns.map((col) => (
          <div key={col.key} className={`border-t-4 ${col.color} bg-white rounded-lg p-4`}>
            <h2 className="font-semibold text-lg mb-4">{col.label}</h2>
            <div className="space-y-3">
              {tasks
                .filter((t) => t.status === col.key)
                .map((task) => (
                  <TaskCard
                    key={task.id}
                    task={task}
                    onStatusChange={handleStatusChange}
                    onDelete={handleDelete}
                  />
                ))}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

المسار الديناميكي: تفاصيل المهمة

// app/routes/tasks.$taskId.tsx
import { createFileRoute, useRouter } from "@tanstack/react-router";
import { getTaskById, updateTask } from "../lib/server-fns";
 
export const Route = createFileRoute("/tasks/$taskId")({
  loader: async ({ params }) => {
    const task = await getTaskById({ data: { id: params.taskId } });
    return { task };
  },
  component: TaskDetailPage,
});
 
function TaskDetailPage() {
  const { task } = Route.useLoaderData();
  const router = useRouter();
  const { taskId } = Route.useParams();
 
  const priorityColors = {
    low: "bg-gray-100 text-gray-700",
    medium: "bg-yellow-100 text-yellow-700",
    high: "bg-red-100 text-red-700",
  };
 
  const statusLabels = {
    todo: "À faire",
    in_progress: "En cours",
    done: "Terminé",
  };
 
  return (
    <div className="max-w-2xl mx-auto">
      <div className="bg-white rounded-xl shadow-sm p-8">
        <div className="flex items-start justify-between mb-6">
          <h1 className="text-2xl font-bold">{task.title}</h1>
          <span
            className={`px-3 py-1 rounded-full text-sm font-medium ${
              priorityColors[task.priority as keyof typeof priorityColors]
            }`}
          >
            {task.priority}
          </span>
        </div>
 
        {task.description && (
          <p className="text-gray-600 mb-6">{task.description}</p>
        )}
 
        <div className="flex items-center gap-4 text-sm text-gray-500 mb-6">
          <span>Par {task.author.name}</span>
          <span>•</span>
          <span>
            {new Date(task.createdAt).toLocaleDateString("fr-FR", {
              day: "numeric",
              month: "long",
              year: "numeric",
            })}
          </span>
        </div>
 
        {/* Sélecteur de statut */}
        <div className="flex gap-2">
          {(["todo", "in_progress", "done"] as const).map((status) => (
            <button
              key={status}
              onClick={async () => {
                await updateTask({ data: { id: taskId, status } });
                router.invalidate();
              }}
              className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
                task.status === status
                  ? "bg-indigo-600 text-white"
                  : "bg-gray-100 text-gray-600 hover:bg-gray-200"
              }`}
            >
              {statusLabels[status]}
            </button>
          ))}
        </div>
      </div>
    </div>
  );
}

المعامل $taskId في اسم الملف (tasks.$taskId.tsx) يتم استخراجه وتنميطه تلقائياً — Route.useParams() يُرجع { taskId: string } دون أي إعداد إضافي.

الخطوة 6: إنشاء مكونات واجهة المستخدم

مكون Header

// app/components/Header.tsx
import { Link } from "@tanstack/react-router";
 
export default function Header() {
  return (
    <header className="bg-white border-b border-gray-200 sticky top-0 z-50">
      <div className="container mx-auto px-4 h-16 flex items-center justify-between">
        <Link to="/" className="text-xl font-bold text-indigo-600">
          TaskBoard
        </Link>
        <nav className="flex items-center gap-6">
          <Link
            to="/"
            className="text-gray-600 hover:text-gray-900 transition-colors"
            activeProps={{ className: "text-indigo-600 font-medium" }}
          >
            Accueil
          </Link>
          <Link
            to="/tasks"
            className="text-gray-600 hover:text-gray-900 transition-colors"
            activeProps={{ className: "text-indigo-600 font-medium" }}
          >
            Tâches
          </Link>
        </nav>
      </div>
    </header>
  );
}

مكون TaskCard

// app/components/TaskCard.tsx
interface Task {
  id: string;
  title: string;
  description: string | null;
  priority: string;
  status: string;
  author: { name: string };
}
 
interface TaskCardProps {
  task: Task;
  onStatusChange: (id: string, status: string) => void;
  onDelete: (id: string) => void;
}
 
export default function TaskCard({ task, onStatusChange, onDelete }: TaskCardProps) {
  const priorityDot = {
    low: "bg-gray-400",
    medium: "bg-yellow-400",
    high: "bg-red-400",
  };
 
  const nextStatus: Record<string, string> = {
    todo: "in_progress",
    in_progress: "done",
    done: "todo",
  };
 
  return (
    <div className="bg-gray-50 rounded-lg p-4 hover:shadow-md transition-shadow">
      <div className="flex items-start justify-between mb-2">
        <h3 className="font-medium text-sm">{task.title}</h3>
        <span
          className={`w-2 h-2 rounded-full mt-1.5 ${
            priorityDot[task.priority as keyof typeof priorityDot]
          }`}
        />
      </div>
 
      {task.description && (
        <p className="text-xs text-gray-500 mb-3 line-clamp-2">
          {task.description}
        </p>
      )}
 
      <div className="flex items-center justify-between">
        <span className="text-xs text-gray-400">{task.author.name}</span>
        <div className="flex gap-1">
          <button
            onClick={() => onStatusChange(task.id, nextStatus[task.status])}
            className="text-xs px-2 py-1 bg-indigo-50 text-indigo-600 rounded hover:bg-indigo-100 transition-colors"
          >
            Avancer
          </button>
          <button
            onClick={() => onDelete(task.id)}
            className="text-xs px-2 py-1 bg-red-50 text-red-600 rounded hover:bg-red-100 transition-colors"
          >
            Supprimer
          </button>
        </div>
      </div>
    </div>
  );
}

نموذج الإنشاء

// app/components/TaskForm.tsx
import { useState } from "react";
import { useRouter } from "@tanstack/react-router";
import { createTask } from "../lib/server-fns";
 
export default function TaskForm({ authorId }: { authorId: string }) {
  const router = useRouter();
  const [isSubmitting, setIsSubmitting] = useState(false);
 
  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setIsSubmitting(true);
 
    const formData = new FormData(e.currentTarget);
 
    await createTask({
      data: {
        title: formData.get("title") as string,
        description: (formData.get("description") as string) || undefined,
        priority: formData.get("priority") as "low" | "medium" | "high",
        authorId,
      },
    });
 
    router.invalidate();
    router.navigate({ to: "/tasks" });
  };
 
  return (
    <form onSubmit={handleSubmit} className="space-y-6 max-w-lg">
      <div>
        <label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-1">
          Titre *
        </label>
        <input
          id="title"
          name="title"
          type="text"
          required
          maxLength={200}
          className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
          placeholder="Ex: Refactoriser le module d'authentification"
        />
      </div>
 
      <div>
        <label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-1">
          Description
        </label>
        <textarea
          id="description"
          name="description"
          rows={4}
          className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
          placeholder="Décrivez les détails de cette tâche..."
        />
      </div>
 
      <div>
        <label htmlFor="priority" className="block text-sm font-medium text-gray-700 mb-1">
          Priorité
        </label>
        <select
          id="priority"
          name="priority"
          defaultValue="medium"
          className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
        >
          <option value="low">Basse</option>
          <option value="medium">Moyenne</option>
          <option value="high">Haute</option>
        </select>
      </div>
 
      <button
        type="submit"
        disabled={isSubmitting}
        className="w-full px-6 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
      >
        {isSubmitting ? "Création en cours..." : "Créer la tâche"}
      </button>
    </form>
  );
}

الخطوة 7: إضافة البرمجيات الوسيطة

تتيح البرمجيات الوسيطة في TanStack Start اعتراض وتعديل سلوك دوال الخادم والمسارات. وهي مثالية للمصادقة، والتسجيل، والتحقق من المدخلات.

// app/lib/middleware.ts
import { createMiddleware } from "@tanstack/react-start";
 
// Middleware de journalisation
export const loggingMiddleware = createMiddleware().server(
  async ({ next }) => {
    const start = Date.now();
    const result = await next();
    const duration = Date.now() - start;
    console.log(`[Server] Requête traitée en ${duration}ms`);
    return result;
  }
);
 
// Middleware d'authentification
export const authMiddleware = createMiddleware()
  .server(async ({ next }) => {
    // Vérifiez le token d'authentification ici
    // Par exemple, lire un cookie de session
    const userId = "user-demo-id"; // Remplacez par votre logique d'auth
 
    if (!userId) {
      throw new Error("Non authentifié");
    }
 
    return next({ context: { userId } });
  });

استخدم البرمجيات الوسيطة في دوال الخادم:

// Exemple d'utilisation avec le middleware
export const createTaskAuthenticated = createServerFn({ method: "POST" })
  .middleware([authMiddleware, loggingMiddleware])
  .validator((data: unknown) => createTaskSchema.parse(data))
  .handler(async ({ data, context }) => {
    // context.userId est disponible grâce au middleware d'auth
    const task = await prisma.task.create({
      data: {
        ...data,
        authorId: context.userId,
      },
    });
    return task;
  });

البرمجيات الوسيطة قابلة للتركيب — يمكنك سلسلة عدة برمجيات وسيطة ويمكن لكل واحدة إثراء السياق المُمرر للتالية.

الخطوة 8: مسارات API

يتيح TanStack Start أيضاً إنشاء مسارات API تقليدية للتكاملات الخارجية:

// app/routes/api/tasks.ts
import { json } from "@tanstack/react-start";
import { createAPIFileRoute } from "@tanstack/react-start/api";
import { prisma } from "../../lib/db";
 
export const APIRoute = createAPIFileRoute("/api/tasks")({
  GET: async () => {
    const tasks = await prisma.task.findMany({
      include: { author: true },
      orderBy: { createdAt: "desc" },
    });
    return json(tasks);
  },
  POST: async ({ request }) => {
    const body = await request.json();
    const task = await prisma.task.create({
      data: body,
      include: { author: true },
    });
    return json(task, { status: 201 });
  },
});

هذه المسارات متاحة عبر GET /api/tasks وPOST /api/tasks، وهو مفيد للـ webhooks، وتطبيقات الهاتف، والتكاملات الخارجية.

الخطوة 9: إدارة الأخطاء

يقدم TanStack Start إدارة أخطاء أنيقة عبر Error Boundaries:

// app/routes/tasks.$taskId.tsx (ajout d'errorComponent)
export const Route = createFileRoute("/tasks/$taskId")({
  loader: async ({ params }) => {
    const task = await getTaskById({ data: { id: params.taskId } });
    return { task };
  },
  component: TaskDetailPage,
  errorComponent: TaskError,
  notFoundComponent: TaskNotFound,
});
 
function TaskError({ error }: { error: Error }) {
  return (
    <div className="max-w-md mx-auto text-center py-12">
      <div className="text-red-500 text-5xl mb-4">!</div>
      <h2 className="text-xl font-bold mb-2">Une erreur est survenue</h2>
      <p className="text-gray-600">{error.message}</p>
    </div>
  );
}
 
function TaskNotFound() {
  return (
    <div className="max-w-md mx-auto text-center py-12">
      <div className="text-gray-400 text-5xl mb-4">?</div>
      <h2 className="text-xl font-bold mb-2">Tâche introuvable</h2>
      <p className="text-gray-600">
        Cette tâche n'existe pas ou a été supprimée.
      </p>
    </div>
  );
}

الخطوة 10: العرض المسبق الثابت وISR

يدعم TanStack Start العرض المسبق الثابت وISR (Incremental Static Regeneration) لأداء مثالي:

// app.config.ts
import { defineConfig } from "@tanstack/react-start/config";
import tailwindcss from "@tailwindcss/vite";
 
export default defineConfig({
  vite: {
    plugins: [tailwindcss()],
  },
  server: {
    prerender: {
      routes: ["/"],
      crawlLinks: true,
    },
  },
});

لاستخدام ISR على مسارات محددة:

// app/routes/index.tsx
export const Route = createFileRoute("/")({
  loader: async () => {
    const tasks = await getTasks();
    return { tasks };
  },
  // Revalider toutes les 60 secondes
  headers: () => ({
    "Cache-Control": "public, max-age=60, stale-while-revalidate=120",
  }),
  component: HomePage,
});

الخطوة 11: النشر على Vercel

يتكامل TanStack Start بسلاسة مع Vercel. ثبّت الـ preset:

npm install @tanstack/react-start-preset-vercel

حدّث الإعدادات:

// app.config.ts
import { defineConfig } from "@tanstack/react-start/config";
import tailwindcss from "@tailwindcss/vite";
 
export default defineConfig({
  vite: {
    plugins: [tailwindcss()],
  },
  server: {
    preset: "vercel",
  },
});

انشر التطبيق:

npm i -g vercel
vercel

سيكتشف Vercel تلقائياً TanStack Start ويُعدّ البناء. سيكون تطبيقك متاحاً في غضون دقائق.

خيارات نشر أخرى

يدعم TanStack Start أيضاً:

  • Cloudflare Workers — مع @tanstack/react-start-preset-cloudflare
  • Netlify — مع @tanstack/react-start-preset-netlify
  • Railway — إعداد Docker قياسي
  • Node.js standalone — للاستضافة التقليدية

اختبار تطبيقك

تعبئة قاعدة البيانات

أنشئ سكربت seed لاختبار التطبيق ببيانات:

// prisma/seed.ts
import { PrismaClient } from "@prisma/client";
 
const prisma = new PrismaClient();
 
async function main() {
  const user = await prisma.user.create({
    data: {
      name: "Ahmed Mansouri",
      email: "ahmed@example.com",
    },
  });
 
  const tasks = [
    {
      title: "Configurer l'environnement de développement",
      description: "Installer Node.js, Docker et les extensions VS Code nécessaires.",
      status: "done",
      priority: "high",
      authorId: user.id,
    },
    {
      title: "Implémenter l'authentification OAuth",
      description: "Ajouter le support Google et GitHub OAuth avec Better Auth.",
      status: "in_progress",
      priority: "high",
      authorId: user.id,
    },
    {
      title: "Écrire les tests unitaires",
      description: "Couvrir les fonctions serveur avec Vitest.",
      status: "todo",
      priority: "medium",
      authorId: user.id,
    },
    {
      title: "Optimiser les performances du dashboard",
      description: "Profiler le rendu et appliquer React.memo si nécessaire.",
      status: "todo",
      priority: "low",
      authorId: user.id,
    },
  ];
 
  for (const task of tasks) {
    await prisma.task.create({ data: task });
  }
 
  console.log("Base de données peuplée avec succès !");
}
 
main()
  .catch(console.error)
  .finally(() => prisma.$disconnect());

شغّل الـ seed:

npx tsx prisma/seed.ts

التحقق

شغّل التطبيق وتحقق من:

  1. الصفحة الرئيسية تعرض الإحصائيات
  2. صفحة /tasks تُظهر أعمدة Kanban
  3. إنشاء المهام يعمل عبر النموذج
  4. تغيير الحالة يُحدّث العرض
  5. الحذف يعمل مع التأكيد
  6. مسارات API تستجيب على /api/tasks

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

الأخطاء الشائعة

"Cannot find module '@prisma/client'"

npx prisma generate

"Port 3000 already in use"

npm run dev -- --port 3001

"Type error in Route.useLoaderData()" تأكد من أن ملف routeTree.gen.ts محدّث:

npm run dev

يُعيد TanStack Start توليد شجرة المسارات تلقائياً عند التشغيل.

الـ HMR لا يعمل تحقق من أن ملف app.config.ts صحيح وأن Vite مُعدّ بشكل سليم. أعد تشغيل خادم التطوير إذا لزم الأمر.

للمضي قدماً

الآن بعد أن أتقنت أساسيات TanStack Start، إليك مسارات للتعمق أكثر:

  • المصادقة الكاملة: ادمج Better Auth أو Lucia لإدارة جلسات متينة
  • الوقت الحقيقي: أضف WebSockets مع Socket.io لتحديثات لوحة Kanban في الوقت الحقيقي
  • الاختبار: استخدم Vitest لاختبار دوال الخادم وPlaywright للاختبارات الشاملة
  • التدويل: استكشف دعم i18n مع Paraglide أو next-intl المُكيّف لـ TanStack Start
  • المراقبة: ادمج Sentry لتتبع الأخطاء في الإنتاج

الخلاصة

في هذا الدليل، تعلمت كيفية:

  1. تهيئة مشروع TanStack Start مع Vite وTypeScript
  2. إعداد قاعدة بيانات مع Prisma وSQLite
  3. إنشاء دوال خادم type-safe مع التحقق عبر Zod
  4. تطبيق التوجيه المبني على الملفات مع مسارات ثابتة وديناميكية
  5. بناء واجهة مستخدم مع مكونات React وTailwind CSS
  6. إضافة برمجيات وسيطة للمصادقة والتسجيل
  7. إنشاء مسارات API للتكاملات الخارجية
  8. إدارة الأخطاء مع Error Boundaries
  9. النشر على Vercel ومنصات أخرى

يمثل TanStack Start تطوراً كبيراً في نظام React البيئي. فلسفته client-first، مقترنة بتوجيه type-safe بالكامل ودوال خادم أنيقة، تجعله خياراً من الدرجة الأولى لتطبيقات full-stack في 2026.

الكود المصدري الكامل لهذا الدليل متاح على GitHub كنقطة انطلاق لمشاريعك الخاصة.


هل تريد قراءة المزيد من الدروس التعليمية؟ تحقق من أحدث درس تعليمي لدينا على بناء تطبيق full-stack مع TanStack Start: إطار عمل React من الجيل القادم.

ناقش مشروعك معنا

نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.

دعنا نجد أفضل الحلول لاحتياجاتك.

مقالات ذات صلة

إضافة المصادقة لتطبيق Next.js 15 باستخدام Auth.js v5: البريد الإلكتروني وOAuth والتحكم بالأدوار

تعلم كيفية إضافة نظام مصادقة جاهز للإنتاج لتطبيق Next.js 15 باستخدام Auth.js v5. يغطي هذا الدليل الشامل تسجيل الدخول عبر Google OAuth وبيانات الاعتماد بالبريد الإلكتروني وكلمة المرور والمسارات المحمية والتحكم بالوصول حسب الأدوار.

30 د قراءة·

بناء وكلاء الذكاء الاصطناعي من الصفر باستخدام TypeScript: إتقان نمط ReAct مع Vercel AI SDK

تعلّم كيفية بناء وكلاء الذكاء الاصطناعي من الأساس باستخدام TypeScript. يغطي هذا الدليل التعليمي نمط ReAct، واستدعاء الأدوات، والاستدلال متعدد الخطوات، وحلقات الوكلاء الجاهزة للإنتاج مع Vercel AI SDK.

35 د قراءة·