React Router v7: بناء تطبيق full-stack مع وضع الإطار (Framework Mode)

AI Bot
بواسطة AI Bot ·

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

يمثل React Router v7 نقطة تحول كبيرة في تطوير React. من خلال اندماجه مع Remix، أصبح إطار عمل full-stack حقيقي قادر على التعامل مع العرض من جانب الخادم (SSR) وتحميل البيانات والتعديلات — كل ذلك بواجهة برمجة موحدة وأنيقة.

في هذا الدرس، ستبني تطبيق إدارة جهات اتصال كامل باستخدام React Router v7 في وضع الإطار (Framework Mode): تحميل البيانات عبر الـ loaders، والتعديلات عبر الـ actions، والتحقق من النماذج، ومعالجة الأخطاء، والعرض من جانب الخادم.

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

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

  • Node.js 20+ مثبت على جهازك
  • npm أو pnpm كمدير حزم
  • معرفة أساسية بـ React و TypeScript
  • إلمام بمفاهيم HTTP (GET، POST، PUT، DELETE)

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

تطبيق إدارة جهات اتصال يتضمن الميزات التالية:

  • قائمة جهات الاتصال مع البحث الفوري
  • إنشاء وتعديل وحذف جهات الاتصال
  • التحقق من النماذج على جانب الخادم
  • معالجة الأخطاء باستخدام Error Boundaries
  • العرض من جانب الخادم (SSR) لتحسين SEO

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

يوفر React Router v7 أداة CLI لإنشاء مشروع جديد في وضع الإطار:

npx create-react-router@latest contacts-app
cd contacts-app

ستطرح عليك الأداة بعض الأسئلة. اختر الخيارات التالية:

  • Template: Basic
  • TypeScript: Yes
  • Package manager: npm (أو pnpm حسب تفضيلك)

هيكل المشروع

إليك الهيكل المُنشأ:

contacts-app/
├── app/
│   ├── routes/
│   │   └── home.tsx
│   ├── root.tsx
│   ├── routes.ts
│   └── app.css
├── react-router.config.ts
├── vite.config.ts
├── tsconfig.json
└── package.json

الملفات الرئيسية هي:

الملفالدور
react-router.config.tsإعدادات الإطار العامة
app/routes.tsتعريف مسارات التطبيق
app/root.tsxالتخطيط الجذري (HTML shell)
app/routes/*.tsxوحدات المسارات (المكونات، loaders، actions)

الخطوة 2: تفعيل SSR

افتح react-router.config.ts وفعّل العرض من جانب الخادم:

import type { Config } from "@react-router/dev/config";
 
export default {
  ssr: true,
} satisfies Config;

مع ssr: true، سيقوم React Router بـ:

  • تنفيذ الـ loaders على الخادم قبل عرض الصفحة
  • إرسال HTML كامل إلى المتصفح (SEO أفضل، تحميل أسرع)
  • ترطيب (Hydrate) الجانب العميل للتفاعلية

الخطوة 3: إنشاء نموذج البيانات

أنشئ ملف app/data/contacts.ts لمحاكاة قاعدة البيانات:

export interface Contact {
  id: string;
  firstName: string;
  lastName: string;
  email: string;
  phone?: string;
  notes?: string;
  createdAt: string;
}
 
const contacts: Map<string, Contact> = new Map();
 
// البيانات الأولية
const initialContacts: Contact[] = [
  {
    id: "1",
    firstName: "أميرة",
    lastName: "بن علي",
    email: "amira@example.com",
    phone: "+216 50 123 456",
    notes: "مطورة واجهات أمامية",
    createdAt: "2026-01-15",
  },
  {
    id: "2",
    firstName: "يوسف",
    lastName: "منصور",
    email: "youssef@example.com",
    phone: "+216 55 789 012",
    notes: "مصمم UX/UI",
    createdAt: "2026-02-01",
  },
  {
    id: "3",
    firstName: "ليلى",
    lastName: "الطرابلسي",
    email: "leila@example.com",
    notes: "مديرة مشاريع",
    createdAt: "2026-02-20",
  },
];
 
initialContacts.forEach((c) => contacts.set(c.id, c));
 
let nextId = 4;
 
export function getContacts(query?: string): Contact[] {
  let result = Array.from(contacts.values());
  if (query) {
    const q = query.toLowerCase();
    result = result.filter(
      (c) =>
        c.firstName.toLowerCase().includes(q) ||
        c.lastName.toLowerCase().includes(q) ||
        c.email.toLowerCase().includes(q)
    );
  }
  return result.sort(
    (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
  );
}
 
export function getContact(id: string): Contact | undefined {
  return contacts.get(id);
}
 
export function createContact(
  data: Omit<Contact, "id" | "createdAt">
): Contact {
  const id = String(nextId++);
  const contact: Contact = {
    ...data,
    id,
    createdAt: new Date().toISOString().split("T")[0],
  };
  contacts.set(id, contact);
  return contact;
}
 
export function updateContact(
  id: string,
  data: Partial<Omit<Contact, "id" | "createdAt">>
): Contact | null {
  const existing = contacts.get(id);
  if (!existing) return null;
  const updated = { ...existing, ...data };
  contacts.set(id, updated);
  return updated;
}
 
export function deleteContact(id: string): boolean {
  return contacts.delete(id);
}

في تطبيق حقيقي، ستستخدم قاعدة بيانات مثل PostgreSQL مع Prisma أو Drizzle ORM. هذه الوحدة في الذاكرة كافية لتعلم مفاهيم React Router v7.

الخطوة 4: تعريف المسارات

افتح app/routes.ts وعرّف هيكل المسارات:

import { type RouteConfig, route, index, layout } from "@react-router/dev/routes";
 
export default [
  layout("routes/layout.tsx", [
    index("routes/home.tsx"),
    route("contacts/new", "routes/contacts-new.tsx"),
    route("contacts/:contactId", "routes/contact-detail.tsx"),
    route("contacts/:contactId/edit", "routes/contact-edit.tsx"),
    route("contacts/:contactId/delete", "routes/contact-delete.tsx"),
  ]),
] satisfies RouteConfig;

كل مسار هو وحدة يمكنها تصدير loader و action ومكون افتراضي و ErrorBoundary.

الخطوة 5: إنشاء التخطيط الرئيسي

أنشئ app/routes/layout.tsx — التخطيط المشترك بين جميع الصفحات:

import { Outlet, Link, useNavigation } from "react-router";
 
export default function AppLayout() {
  const navigation = useNavigation();
  const isNavigating = navigation.state !== "idle";
 
  return (
    <div className="app-layout">
      <header className="app-header">
        <Link to="/" className="logo">
          <h1>جهات الاتصال</h1>
        </Link>
        <nav>
          <Link to="/contacts/new" className="btn btn-primary">
            + جهة اتصال جديدة
          </Link>
        </nav>
      </header>
 
      <main className={isNavigating ? "loading" : ""}>
        <Outlet />
      </main>
 
      <footer className="app-footer">
        <p>مبني باستخدام React Router v7</p>
      </footer>
    </div>
  );
}

مكون <Outlet /> ضروري: فهو يعرض محتوى المسار الفرعي النشط. يتيح لك hook useNavigation() اكتشاف الانتقالات وعرض حالة التحميل.

الخطوة 6: الصفحة الرئيسية مع loader والبحث

استبدل محتوى app/routes/home.tsx:

import { useLoaderData, Form, useSearchParams } from "react-router";
import { getContacts } from "~/data/contacts";
import type { Route } from "./+types/home";
 
export async function loader({ request }: Route.LoaderArgs) {
  const url = new URL(request.url);
  const query = url.searchParams.get("q") || undefined;
  const contacts = getContacts(query);
  return { contacts, query };
}
 
export default function Home() {
  const { contacts, query } = useLoaderData<typeof loader>();
  const [searchParams] = useSearchParams();
 
  return (
    <div className="home-page">
      <div className="search-section">
        <Form method="get" className="search-form">
          <input
            type="search"
            name="q"
            placeholder="ابحث عن جهة اتصال..."
            defaultValue={query || ""}
            aria-label="البحث عن جهات الاتصال"
          />
          <button type="submit">بحث</button>
        </Form>
      </div>
 
      <div className="contacts-list">
        <h2>
          {query
            ? `نتائج البحث عن "${query}" (${contacts.length})`
            : `جميع جهات الاتصال (${contacts.length})`}
        </h2>
 
        {contacts.length === 0 ? (
          <p className="empty-state">
            {query
              ? "لم يتم العثور على جهات اتصال لهذا البحث."
              : "لا توجد جهات اتصال بعد. أنشئ واحدة!"}
          </p>
        ) : (
          <ul className="contact-cards">
            {contacts.map((contact) => (
              <li key={contact.id}>
                <a href={`/contacts/${contact.id}`} className="contact-card">
                  <div className="contact-avatar">
                    {contact.firstName[0]}
                    {contact.lastName[0]}
                  </div>
                  <div className="contact-info">
                    <strong>
                      {contact.firstName} {contact.lastName}
                    </strong>
                    <span>{contact.email}</span>
                  </div>
                </a>
              </li>
            ))}
          </ul>
        )}
      </div>
    </div>
  );
}

كيف يعمل الـ loader؟

  1. يتم تنفيذ الـ loader على الخادم قبل عرض المكون
  2. يستقبل كائن request مع جميع معاملات طلب HTTP
  3. البيانات المُرجعة يتم تسلسلها تلقائياً ويمكن الوصول إليها عبر useLoaderData()
  4. مكون <Form method="get"> يرسل نموذج GET يحدّث معاملات البحث بدون إعادة تحميل كاملة

الخطوة 7: نموذج الإنشاء مع action

أنشئ app/routes/contacts-new.tsx:

import { Form, redirect, useActionData, useNavigation } from "react-router";
import { createContact } from "~/data/contacts";
import type { Route } from "./+types/contacts-new";
 
interface ActionErrors {
  firstName?: string;
  lastName?: string;
  email?: string;
}
 
export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const firstName = String(formData.get("firstName") || "").trim();
  const lastName = String(formData.get("lastName") || "").trim();
  const email = String(formData.get("email") || "").trim();
  const phone = String(formData.get("phone") || "").trim();
  const notes = String(formData.get("notes") || "").trim();
 
  // التحقق على جانب الخادم
  const errors: ActionErrors = {};
 
  if (!firstName) {
    errors.firstName = "الاسم الأول مطلوب";
  }
  if (!lastName) {
    errors.lastName = "اسم العائلة مطلوب";
  }
  if (!email) {
    errors.email = "البريد الإلكتروني مطلوب";
  } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
    errors.email = "صيغة البريد الإلكتروني غير صالحة";
  }
 
  if (Object.keys(errors).length > 0) {
    return { errors };
  }
 
  const contact = createContact({
    firstName,
    lastName,
    email,
    phone: phone || undefined,
    notes: notes || undefined,
  });
 
  return redirect(`/contacts/${contact.id}`);
}
 
export default function NewContact() {
  const actionData = useActionData<typeof action>();
  const navigation = useNavigation();
  const isSubmitting = navigation.state === "submitting";
  const errors = actionData?.errors;
 
  return (
    <div className="form-page">
      <h2>جهة اتصال جديدة</h2>
 
      <Form method="post" className="contact-form">
        <div className="form-group">
          <label htmlFor="firstName">الاسم الأول *</label>
          <input
            id="firstName"
            name="firstName"
            type="text"
            required
            aria-invalid={errors?.firstName ? true : undefined}
            aria-describedby={
              errors?.firstName ? "firstName-error" : undefined
            }
          />
          {errors?.firstName && (
            <p id="firstName-error" className="error-message">
              {errors.firstName}
            </p>
          )}
        </div>
 
        <div className="form-group">
          <label htmlFor="lastName">اسم العائلة *</label>
          <input
            id="lastName"
            name="lastName"
            type="text"
            required
            aria-invalid={errors?.lastName ? true : undefined}
          />
          {errors?.lastName && (
            <p className="error-message">{errors.lastName}</p>
          )}
        </div>
 
        <div className="form-group">
          <label htmlFor="email">البريد الإلكتروني *</label>
          <input
            id="email"
            name="email"
            type="email"
            required
            aria-invalid={errors?.email ? true : undefined}
          />
          {errors?.email && (
            <p className="error-message">{errors.email}</p>
          )}
        </div>
 
        <div className="form-group">
          <label htmlFor="phone">الهاتف</label>
          <input id="phone" name="phone" type="tel" />
        </div>
 
        <div className="form-group">
          <label htmlFor="notes">ملاحظات</label>
          <textarea id="notes" name="notes" rows={3} />
        </div>
 
        <div className="form-actions">
          <button type="submit" className="btn btn-primary" disabled={isSubmitting}>
            {isSubmitting ? "جاري الإنشاء..." : "إنشاء جهة الاتصال"}
          </button>
          <a href="/" className="btn btn-secondary">
            إلغاء
          </a>
        </div>
      </Form>
    </div>
  );
}

النقاط الرئيسية في هذا الـ action

  • <Form method="post"> يرسل البيانات عبر طلب POST يعترضه React Router
  • action() يتم تنفيذها على الخادم وتستقبل بيانات النموذج
  • التحقق يتم على جانب الخادم — الأخطاء تُرجع ويمكن الوصول إليها عبر useActionData()
  • عند النجاح، redirect() يعيد التوجيه إلى صفحة جهة الاتصال الجديدة
  • useNavigation() يتيح تعطيل الزر أثناء الإرسال

الخطوة 8: صفحة تفاصيل جهة الاتصال

أنشئ app/routes/contact-detail.tsx:

import { useLoaderData, Link } from "react-router";
import { getContact } from "~/data/contacts";
import type { Route } from "./+types/contact-detail";
 
export async function loader({ params }: Route.LoaderArgs) {
  const contact = getContact(params.contactId);
  if (!contact) {
    throw new Response("جهة الاتصال غير موجودة", { status: 404 });
  }
  return { contact };
}
 
export default function ContactDetail() {
  const { contact } = useLoaderData<typeof loader>();
 
  return (
    <div className="detail-page">
      <div className="contact-header">
        <div className="contact-avatar large">
          {contact.firstName[0]}
          {contact.lastName[0]}
        </div>
        <div>
          <h2>
            {contact.firstName} {contact.lastName}
          </h2>
          <p className="contact-email">{contact.email}</p>
        </div>
      </div>
 
      <div className="contact-details">
        {contact.phone && (
          <div className="detail-row">
            <span className="label">الهاتف</span>
            <span>{contact.phone}</span>
          </div>
        )}
 
        {contact.notes && (
          <div className="detail-row">
            <span className="label">ملاحظات</span>
            <p>{contact.notes}</p>
          </div>
        )}
 
        <div className="detail-row">
          <span className="label">تاريخ الإضافة</span>
          <span>
            {new Date(contact.createdAt).toLocaleDateString("ar-TN", {
              year: "numeric",
              month: "long",
              day: "numeric",
            })}
          </span>
        </div>
      </div>
 
      <div className="detail-actions">
        <Link to={`/contacts/${contact.id}/edit`} className="btn btn-primary">
          تعديل
        </Link>
        <Link to={`/contacts/${contact.id}/delete`} className="btn btn-danger">
          حذف
        </Link>
        <Link to="/" className="btn btn-secondary">
          رجوع
        </Link>
      </div>
    </div>
  );
}
 
export function ErrorBoundary() {
  return (
    <div className="error-page">
      <h2>جهة الاتصال غير موجودة</h2>
      <p>جهة الاتصال التي تبحث عنها غير موجودة أو تم حذفها.</p>
      <Link to="/" className="btn btn-primary">
        العودة إلى القائمة
      </Link>
    </div>
  );
}

إلقاء Response بحالة 404 في الـ loader يؤدي تلقائياً إلى تشغيل أقرب ErrorBoundary. هذا هو النمط القياسي للتعامل مع الموارد غير الموجودة في React Router v7.

الخطوة 9: التعديل مع action و loader مجتمعين

أنشئ app/routes/contact-edit.tsx:

import { Form, redirect, useLoaderData, useActionData, useNavigation } from "react-router";
import { getContact, updateContact } from "~/data/contacts";
import type { Route } from "./+types/contact-edit";
 
export async function loader({ params }: Route.LoaderArgs) {
  const contact = getContact(params.contactId);
  if (!contact) {
    throw new Response("جهة الاتصال غير موجودة", { status: 404 });
  }
  return { contact };
}
 
export async function action({ request, params }: Route.ActionArgs) {
  const formData = await request.formData();
  const firstName = String(formData.get("firstName") || "").trim();
  const lastName = String(formData.get("lastName") || "").trim();
  const email = String(formData.get("email") || "").trim();
  const phone = String(formData.get("phone") || "").trim();
  const notes = String(formData.get("notes") || "").trim();
 
  const errors: Record<string, string> = {};
  if (!firstName) errors.firstName = "الاسم الأول مطلوب";
  if (!lastName) errors.lastName = "اسم العائلة مطلوب";
  if (!email) errors.email = "البريد الإلكتروني مطلوب";
 
  if (Object.keys(errors).length > 0) {
    return { errors };
  }
 
  const updated = updateContact(params.contactId, {
    firstName,
    lastName,
    email,
    phone: phone || undefined,
    notes: notes || undefined,
  });
 
  if (!updated) {
    throw new Response("جهة الاتصال غير موجودة", { status: 404 });
  }
 
  return redirect(`/contacts/${params.contactId}`);
}
 
export default function EditContact() {
  const { contact } = useLoaderData<typeof loader>();
  const actionData = useActionData<typeof action>();
  const navigation = useNavigation();
  const isSubmitting = navigation.state === "submitting";
  const errors = actionData?.errors;
 
  return (
    <div className="form-page">
      <h2>تعديل {contact.firstName} {contact.lastName}</h2>
 
      <Form method="post" className="contact-form">
        <div className="form-group">
          <label htmlFor="firstName">الاسم الأول *</label>
          <input
            id="firstName"
            name="firstName"
            type="text"
            defaultValue={contact.firstName}
            required
            aria-invalid={errors?.firstName ? true : undefined}
          />
          {errors?.firstName && (
            <p className="error-message">{errors.firstName}</p>
          )}
        </div>
 
        <div className="form-group">
          <label htmlFor="lastName">اسم العائلة *</label>
          <input
            id="lastName"
            name="lastName"
            type="text"
            defaultValue={contact.lastName}
            required
          />
          {errors?.lastName && (
            <p className="error-message">{errors.lastName}</p>
          )}
        </div>
 
        <div className="form-group">
          <label htmlFor="email">البريد الإلكتروني *</label>
          <input
            id="email"
            name="email"
            type="email"
            defaultValue={contact.email}
            required
          />
          {errors?.email && (
            <p className="error-message">{errors.email}</p>
          )}
        </div>
 
        <div className="form-group">
          <label htmlFor="phone">الهاتف</label>
          <input
            id="phone"
            name="phone"
            type="tel"
            defaultValue={contact.phone || ""}
          />
        </div>
 
        <div className="form-group">
          <label htmlFor="notes">ملاحظات</label>
          <textarea
            id="notes"
            name="notes"
            rows={3}
            defaultValue={contact.notes || ""}
          />
        </div>
 
        <div className="form-actions">
          <button type="submit" className="btn btn-primary" disabled={isSubmitting}>
            {isSubmitting ? "جاري الحفظ..." : "حفظ التغييرات"}
          </button>
          <a href={`/contacts/${contact.id}`} className="btn btn-secondary">
            إلغاء
          </a>
        </div>
      </Form>
    </div>
  );
}

وحدة المسار هذه توضح تماماً قوة React Router v7: ملف واحد يحتوي على الـ loader (تحميل البيانات) و الـ action (التعديل) و المكون (الواجهة). كل شيء موجود في مكان واحد وآمن من ناحية الأنواع.

الخطوة 10: الحذف مع تأكيد

أنشئ app/routes/contact-delete.tsx:

import { Form, redirect, useLoaderData } from "react-router";
import { getContact, deleteContact } from "~/data/contacts";
import type { Route } from "./+types/contact-delete";
 
export async function loader({ params }: Route.LoaderArgs) {
  const contact = getContact(params.contactId);
  if (!contact) {
    throw new Response("جهة الاتصال غير موجودة", { status: 404 });
  }
  return { contact };
}
 
export async function action({ params }: Route.ActionArgs) {
  deleteContact(params.contactId);
  return redirect("/");
}
 
export default function DeleteContact() {
  const { contact } = useLoaderData<typeof loader>();
 
  return (
    <div className="confirm-page">
      <h2>تأكيد الحذف</h2>
      <p>
        هل أنت متأكد من حذف جهة الاتصال{" "}
        <strong>
          {contact.firstName} {contact.lastName}
        </strong>
        ؟ لا يمكن التراجع عن هذا الإجراء.
      </p>
 
      <div className="confirm-actions">
        <Form method="post">
          <button type="submit" className="btn btn-danger">
            نعم، احذف
          </button>
        </Form>
        <a href={`/contacts/${contact.id}`} className="btn btn-secondary">
          إلغاء
        </a>
      </div>
    </div>
  );
}

الخطوة 11: معالجة الأخطاء الشاملة

عدّل app/root.tsx لإضافة معالجة شاملة للأخطاء:

import {
  Links,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
  isRouteErrorResponse,
} from "react-router";
import type { Route } from "./+types/root";
import "./app.css";
 
export function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ar" dir="rtl">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        {children}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}
 
export default function App() {
  return <Outlet />;
}
 
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
  if (isRouteErrorResponse(error)) {
    return (
      <div className="error-container">
        <h1>
          {error.status} {error.statusText}
        </h1>
        <p>{error.data}</p>
        <a href="/">العودة إلى الصفحة الرئيسية</a>
      </div>
    );
  }
 
  return (
    <div className="error-container">
      <h1>خطأ غير متوقع</h1>
      <p>حدث خطأ غير متوقع. يرجى المحاولة مرة أخرى.</p>
      <a href="/">العودة إلى الصفحة الرئيسية</a>
    </div>
  );
}

الخطوة 12: التشغيل والاختبار

شغّل خادم التطوير:

npm run dev

تطبيقك متاح على http://localhost:5173. اختبر الميزات:

  1. الصفحة الرئيسية — قائمة جهات الاتصال تُحمّل عبر الـ loader SSR
  2. البحث — اكتب اسماً في شريط البحث، النتائج تتحدث فوراً
  3. الإنشاء — انقر على "جهة اتصال جديدة"، املأ النموذج
  4. التحقق — أرسل نموذجاً غير مكتمل لرؤية أخطاء الخادم
  5. التعديل — افتح جهة اتصال وعدّل معلوماتها
  6. الحذف — احذف جهة اتصال من صفحة التأكيد

المفاهيم الأساسية للتذكر

Loader مقابل Action

المفهومطريقة HTTPالدورالتنفيذ
loaderGETتحميل البياناتقبل العرض
actionPOST, PUT, DELETEتعديل البياناتعند إرسال النموذج

إعادة التحقق التلقائية

بعد كل action، يقوم React Router بإعادة التحقق تلقائياً من جميع الـ loaders النشطة. هذا يعني أنه عند إنشاء جهة اتصال، تتحدث القائمة في الصفحة الرئيسية تلقائياً — بدون أي كود إضافي.

أمان الأنواع (Type Safety)

يولّد React Router v7 تلقائياً أنواعاً لكل وحدة مسار في مجلد .react-router/types/. هذا يمنحك كتابة كاملة لـ:

  • معاملات المسار (params.contactId)
  • وسيطات الـ loader والـ action
  • البيانات المُرجعة من useLoaderData()

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

الأنواع لا تُولّد

شغّل الأمر التالي لإعادة توليد الأنواع:

npx react-router typegen

خطأ "Cannot find module ./+types/"

تأكد أن tsconfig.json يتضمن المسارات المولّدة تلقائياً:

{
  "compilerOptions": {
    "rootDirs": [".", "./.react-router/types"]
  }
}

الـ SSR لا يعمل

تحقق من أن ssr: true محدد في react-router.config.ts وأنك تستخدم loader (وليس clientLoader).

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

الآن بعد أن أتقنت أساسيات React Router v7 في وضع الإطار، إليك مسارات للتعمق أكثر:

  • إضافة قاعدة بيانات: استبدل التخزين في الذاكرة بـ Prisma + PostgreSQL أو Drizzle ORM + SQLite
  • المصادقة: طبّق نظام جلسات باستخدام الكوكيز
  • واجهة متفائلة (Optimistic UI): استخدم useFetcher() للتحديثات المتفائلة بدون تنقل
  • النشر: انشر على Vercel أو Cloudflare Workers أو خادم VPS مع Docker
  • React Server Components: استكشف الدعم التجريبي لـ RSC في React Router v7

الخلاصة

يمثل React Router v7 في وضع الإطار نهجاً حديثاً وموحداً لبناء تطبيقات React full-stack. من خلال دمج الـ loaders والـ actions والـ SSR في وحدات مسار متجاورة، يبسّط التطوير بشكل كبير مع تقديم أداء ممتاز وكتابة أنواع كاملة.

المفاهيم التي تعلمتها في هذا الدرس — تحميل البيانات من الخادم، التحقق من النماذج، التعديلات، ومعالجة الأخطاء — تشكل الأساس لبناء تطبيقات ويب متينة وعالية الأداء باستخدام React.


هل تريد قراءة المزيد من الدروس التعليمية؟ تحقق من أحدث درس تعليمي لدينا على محول MCP لووردبريس: اجعل موقعك جاهزا لوكلاء الذكاء الاصطناعي.

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

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

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

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