تحوير البيانات

في الفصل السابق، قمت بتنفيذ البحث والترقيم باستخدام معلمات البحث في URL وواجهات برمجة التطبيقات (APIs) في Next.js. دعونا نواصل العمل على صفحة الفواتير بإضافة القدرة على إنشاء وتحديث وحذف الفواتير!

ما هي إجراءات الخادم؟

تسمح لك إجراءات الخادم في React بتشغيل الكود غير المتزامن مباشرة على الخادم. إنها تلغي الحاجة إلى إنشاء نقاط نهاية API لتحوير بياناتك. بدلاً من ذلك، تكتب دوال غير متزامنة تنفذ على الخادم ويمكن استدعاؤها من مكونات العميل أو الخادم.

الأمان هو أولوية قصوى للتطبيقات الويب، حيث يمكن أن تكون عرضة لمختلف التهديدات. هنا تأتي أهمية إجراءات الخادم. فهي تتضمن ميزات مثل الإغلاقات المشفرة، وفحوصات صارمة للإدخال، وتجزئة رسائل الخطأ، وقيود المضيف، والمزيد - كلها تعمل معًا لتعزيز أمان تطبيقك بشكل كبير.

استخدام النماذج مع إجراءات الخادم

في React، يمكنك استخدام سمة action في عنصر <form> لاستدعاء الإجراءات. سيتلقى الإجراء تلقائيًا كائن FormData الأصلي، الذي يحتوي على البيانات التي تم التقاطها.

على سبيل المثال:

// مكون الخادم
export default function Page() {
  // الإجراء
  async function create(formData: FormData) {
    'use server';
 
    // منطق تحوير البيانات...
  }
 
  // استدعاء الإجراء باستخدام سمة "action"
  return <form action={create}>...</form>;
}

ميزة استدعاء إجراء الخادم داخل مكون الخادم هي التحسين التدريجي - تعمل النماذج حتى إذا لم يتم تحميل JavaScript بعد على العميل. على سبيل المثال، مع اتصالات الإنترنت البطيئة.

Next.js مع إجراءات الخادم

إجراءات الخادم متكاملة بعمق مع ذاكرة التخزين المؤقت في Next.js. عند تقديم نموذج عبر إجراء خادم، لا يمكنك فقط استخدام الإجراء لتحوير البيانات، ولكن يمكنك أيضًا إعادة التحقق من ذاكرة التخزين المؤقت المرتبطة باستخدام واجهات برمجة التطبيقات مثل revalidatePath و revalidateTag.

دعونا نرى كيف يعمل كل هذا معًا!

إنشاء فاتورة

إليك الخطوات التي ستتخذها لإنشاء فاتورة جديدة:

  1. إنشاء نموذج لالتقاط إدخال المستخدم.
  2. إنشاء إجراء خادم واستدعاؤه من النموذج.
  3. داخل إجراء الخادم الخاص بك، استخرج البيانات من كائن formData.
  4. تحقق من صحة البيانات وجهزها للإدراج في قاعدة البيانات الخاصة بك.
  5. أدخل البيانات وتعامل مع أي أخطاء.
  6. أعد التحقق من ذاكرة التخزين المؤقت وأعد توجيه المستخدم إلى صفحة الفواتير.

1. إنشاء مسار ونموذج جديد

للبدء، داخل مجلد /invoices، أضف مقطع مسار جديد يسمى /create مع ملف page.tsx:

مجلد الفواتير مع مجلد فرعي باسم create، وملف page.tsx بداخله

ستستخدم هذا المسار لإنشاء فواتير جديدة. داخل ملف page.tsx الخاص بك، الصق الكود التالي، ثم خذ بعض الوقت لدراسته:

/dashboard/invoices/create/page.tsx
import Form from '@/app/ui/invoices/create-form';
import Breadcrumbs from '@/app/ui/invoices/breadcrumbs';
import { fetchCustomers } from '@/app/lib/data';
 
export default async function Page() {
  const customers = await fetchCustomers();
 
  return (
    <main>
      <Breadcrumbs
        breadcrumbs={[
          { label: 'الفواتير', href: '/dashboard/invoices' },
          {
            label: 'إنشاء فاتورة',
            href: '/dashboard/invoices/create',
            active: true,
          },
        ]}
      />
      <Form customers={customers} />
    </main>
  );
}

صفحتك هي مكون خادم يسترد customers ويمرره إلى مكون <Form>. لتوفير الوقت، قمنا بالفعل بإنشاء مكون <Form> لك.

انتقل إلى مكون <Form>، وسترى أن النموذج:

  • يحتوي على عنصر <select> واحد (قائمة منسدلة) مع قائمة العملاء.
  • يحتوي على عنصر <input> واحد لـ المبلغ مع type="number".
  • يحتوي على عنصرين <input> للحالة مع type="radio".
  • يحتوي على زر واحد مع type="submit".

على http://localhost:3000/dashboard/invoices/create، يجب أن ترى واجهة المستخدم التالية:

صفحة إنشاء الفواتير مع مسار التنقل والنموذج

2. إنشاء إجراء خادم

رائع، الآن دعونا ننشئ إجراء خادم سيتم استدعاؤه عند تقديم النموذج.

انتقل إلى دليل lib/ الخاص بك وأنشئ ملفًا جديدًا باسم actions.ts. في أعلى هذا الملف، أضف توجيه React use server:

/app/lib/actions.ts
'use server';

بإضافة 'use server'، تقوم بتمييز جميع الدوال المصدرة داخل الملف كإجراءات خادم. يمكن بعد ذلك استيراد هذه دوال الخادم واستخدامها في مكونات العميل والخادم. أي دوال مدرجة في هذا الملف ولا يتم استخدامها ستتم إزالتها تلقائيًا من حزمة التطبيق النهائية.

يمكنك أيضًا كتابة إجراءات الخادم مباشرة داخل مكونات الخادم عن طريق إضافة "use server" داخل الإجراء. ولكن لهذه الدورة، سنحتفظ بها جميعًا منظمة في ملف منفصل. نوصي بوجود ملف منفصل لإجراءاتك.

في ملف actions.ts الخاص بك، أنشئ دالة غير متزامنة جديدة تقبل formData:

/app/lib/actions.ts
'use server';
 
export async function createInvoice(formData: FormData) {}

ثم، في مكون <Form> الخاص بك، قم باستيراد createInvoice من ملف actions.ts. أضف سمة action إلى عنصر <form>، واستدع إجراء createInvoice.

/app/ui/invoices/create-form.tsx
import { CustomerField } from '@/app/lib/definitions';
import Link from 'next/link';
import {
  CheckIcon,
  ClockIcon,
  CurrencyDollarIcon,
  UserCircleIcon,
} from '@heroicons/react/24/outline';
import { Button } from '@/app/ui/button';
import { createInvoice } from '@/app/lib/actions';
 
export default function Form({
  customers,
}: {
  customers: CustomerField[];
}) {
  return (
    <form action={createInvoice}>
      // ...
  )
}

جيد أن تعرف: في HTML، يمكنك تمرير عنوان URL إلى سمة action. سيكون هذا العنوان URL هو الوجهة التي يجب إرسال بيانات النموذج إليها (عادةً نقطة نهاية API).

ومع ذلك، في React، تعتبر سمة action خاصية خاصة - مما يعني أن React تبني عليها للسماح باستدعاء الإجراءات.

خلف الكواليس، تقوم إجراءات الخادم بإنشاء نقطة نهاية POST API. هذا هو السبب في أنك لا تحتاج إلى إنشاء نقاط نهاية API يدويًا عند استخدام إجراءات الخادم.

3. استخراج البيانات من formData

بالعودة إلى ملف actions.ts الخاص بك، ستحتاج إلى استخراج قيم formData، هناك بضعة طرق يمكنك استخدامها. لهذا المثال، دعونا نستخدم طريقة .get(name).

/app/lib/actions.ts
'use server';
 
export async function createInvoice(formData: FormData) {
  const rawFormData = {
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  };
  // اختبره:
  console.log(rawFormData);
}

نصيحة: إذا كنت تعمل مع نماذج تحتوي على العديد من الحقول، فقد ترغب في التفكير في استخدام طريقة entries() مع Object.fromEntries() في JavaScript.

للتحقق من أن كل شيء متصل بشكل صحيح، جرب النموذج. بعد الإرسال، يجب أن ترى البيانات التي أدخلتها للتو في النموذج مسجلة في الطرفية (وليس المتصفح).

الآن بعد أن أصبحت بياناتك في شكل كائن، سيكون العمل معها أسهل بكثير.

4. التحقق من صحة البيانات وإعدادها

قبل إرسال بيانات النموذج إلى قاعدة البيانات الخاصة بك، تريد التأكد من أنها بالشكل الصحيح وبالأنواع الصحيحة. إذا كنت تتذكر من بداية الدورة، فإن جدول الفواتير الخاص بك يتوقع بيانات بالشكل التالي:

/app/lib/definitions.ts
export type Invoice = {
  id: string; // سيتم إنشاؤه في قاعدة البيانات
  customer_id: string;
  amount: number; // مخزنة بالسنتات
  status: 'pending' | 'paid';
  date: string;
};

حتى الآن، لديك فقط customer_id و amount و status من النموذج.

التحقق من النوع والإكراه

من المهم التحقق من أن البيانات من نموذجك تتماشى مع الأنواع المتوقعة في قاعدة البيانات الخاصة بك. على سبيل المثال، إذا أضفت console.log داخل إجراءك:

console.log(typeof rawFormData.amount);

ستلاحظ أن amount من نوع string وليس number. هذا لأن عناصر input مع type="number" ترجع في الواقع سلسلة، وليس رقمًا!

للتعامل مع التحقق من النوع، لديك بعض الخيارات. بينما يمكنك التحقق من الأنواع يدويًا، فإن استخدام مكتبة للتحقق من النوع يمكن أن يوفر لك الوقت والجهد. لمثالنا، سنستخدم Zod، وهي مكتبة تحقق من النوع TypeScript-first يمكنها تبسيط هذه المهمة لك.

في ملف actions.ts الخاص بك، قم باستيراد Zod وحدد مخططًا يتطابق مع شكل كائن النموذج الخاص بك. سيتحقق هذا المخطط من صحة formData قبل حفظها في قاعدة بيانات.

/app/lib/actions.ts
'use server';
 
import { z } from 'zod';
 
const FormSchema = z.object({
  id: z.string(),
  customerId: z.string(),
  amount: z.coerce.number(),
  status: z.enum(['pending', 'paid']),
  date: z.string(),
});
 
const CreateInvoice = FormSchema.omit({ id: true, date: true });
 
export async function createInvoice(formData: FormData) {
  // ...
}

تم تعيين حقل amount خصيصًا للإكراه (التغيير) من سلسلة إلى رقم مع التحقق من نوعه أيضًا.

يمكنك بعد ذلك تمرير rawFormData إلى CreateInvoice للتحقق من الأنواع:

/app/lib/actions.ts
// ...
export async function createInvoice(formData: FormData) {
  const { customerId, amount, status } = CreateInvoice.parse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });
}

تخزين القيم بالسنتات

من الممارسات الجيدة عادةً تخزين القيم النقدية بالسنتات في قاعدة البيانات الخاصة بك لتجنب أخطاء الفاصلة العائمة في JavaScript وضمان دقة أكبر.

لنحول المبلغ إلى سنتات:

/app/lib/actions.ts
// ...
export async function createInvoice(formData: FormData) {
  const { customerId, amount, status } = CreateInvoice.parse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });
  const amountInCents = amount * 100;
}

إنشاء تواريخ جديدة

أخيرًا، لننشئ تاريخًا جديدًا بتنسيق "YYYY-MM-DD" لتاريخ إنشاء الفاتورة:

/app/lib/actions.ts
// ...
export async function createInvoice(formData: FormData) {
  const { customerId, amount, status } = CreateInvoice.parse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });
  const amountInCents = amount * 100;
  const date = new Date().toISOString().split('T')[0];
}

5. إدراج البيانات في قاعدة البيانات الخاصة بك

الآن بعد أن أصبح لديك جميع القيم التي تحتاجها لقاعدة البيانات الخاصة بك، يمكنك إنشاء استعلام SQL لإدراج الفاتورة الجديدة في قاعدة البيانات الخاصة بك وتمرير المتغيرات:

/app/lib/actions.ts
import { z } from 'zod';
import postgres from 'postgres';
 
const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });
 
// ...
 
export async function createInvoice(formData: FormData) {
  const { customerId, amount, status } = CreateInvoice.parse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });
  const amountInCents = amount * 100;
  const date = new Date().toISOString().split('T')[0];
 
  await sql`
    INSERT INTO invoices (customer_id, amount, status, date)
    VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
  `;
}

حاليًا، نحن لا نتعامل مع أي أخطاء. سنتحدث عن هذا في الفصل التالي. في الوقت الحالي، دعنا ننتقل إلى الخطوة التالية.

6. إعادة التحقق وإعادة التوجيه

يمتلك Next.js ذاكرة تخزين مؤقت لموجه العميل (client-side router cache) تقوم بتخزين مقاطع المسار في متصفح المستخدم لفترة محددة. إلى جانب الجلب المسبق (prefetching)، تضمن هذه الذاكرة المؤقتة إمكانية تنقل المستخدمين بسرعة بين المسارات مع تقليل عدد الطلبات المرسلة إلى الخادم.

بما أنك تقوم بتحديث البيانات المعروضة في مسار الفواتير، فأنت تريد مسح هذه الذاكرة المؤقتة وتشغيل طلب جديد إلى الخادم. يمكنك القيام بذلك باستخدام دالة revalidatePath من Next.js:

/app/lib/actions.ts
'use server';
 
import { z } from 'zod';
import { revalidatePath } from 'next/cache';
import postgres from 'postgres';
 
const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });
 
// ...
 
export async function createInvoice(formData: FormData) {
  const { customerId, amount, status } = CreateInvoice.parse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });
  const amountInCents = amount * 100;
  const date = new Date().toISOString().split('T')[0];
 
  await sql`
    INSERT INTO invoices (customer_id, amount, status, date)
    VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
  `;
 
  revalidatePath('/dashboard/invoices');
}

بمجرد تحديث قاعدة البيانات، سيتم إعادة التحقق من مسار /dashboard/invoices، وسيتم جلب بيانات جديدة من الخادم.

في هذه المرحلة، تريد أيضًا إعادة توجيه المستخدم إلى صفحة /dashboard/invoices. يمكنك القيام بذلك باستخدام دالة redirect من Next.js:

/app/lib/actions.ts
'use server';
 
import { z } from 'zod';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import postgres from 'postgres';
 
const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });
 
// ...
 
export async function createInvoice(formData: FormData) {
  // ...
 
  revalidatePath('/dashboard/invoices');
  redirect('/dashboard/invoices');
}

تهانينا! لقد قمت للتو بتنفيذ أول إجراء خادم (Server Action) لك. اختبره عن طريق إضافة فاتورة جديدة، إذا كان كل شيء يعمل بشكل صحيح:

  1. يجب أن يتم إعادة توجيهك إلى مسار /dashboard/invoices عند الإرسال.
  2. يجب أن ترى الفاتورة الجديدة في أعلى الجدول.

تحديث فاتورة

نموذج تحديث الفاتورة مشابه لنموذج إنشاء فاتورة، إلا أنك ستحتاج إلى تمرير معرّف الفاتورة id لتحديث السجل في قاعدة البيانات الخاصة بك. دعونا نرى كيف يمكنك الحصول على معرّف الفاتورة id وتمريره.

هذه هي الخطوات التي ستتخذها لتحديث فاتورة:

  1. إنشاء مقطع مسار ديناميكي جديد مع معرّف الفاتورة id.
  2. قراءة معرّف الفاتورة id من معلمات الصفحة.
  3. جلب الفاتورة المحددة من قاعدة البيانات الخاصة بك.
  4. تعبئة النموذج مسبقًا ببيانات الفاتورة.
  5. تحديث بيانات الفاتورة في قاعدة البيانات الخاصة بك.

1. إنشاء مقطع مسار ديناميكي مع معرّف الفاتورة id

يسمح لك Next.js بإنشاء مقاطع مسار ديناميكية (Dynamic Route Segments) عندما لا تعرف اسم المقطع بالضبط وتريد إنشاء مسارات بناءً على البيانات. يمكن أن يكون هذا عناوين مدونة، صفحات منتجات، إلخ. يمكنك إنشاء مقاطع مسار ديناميكية عن طريق لف اسم مجلد بين أقواس مربعة. على سبيل المثال، [id]، [post] أو [slug].

في مجلد /invoices الخاص بك، قم بإنشاء مسار ديناميكي جديد يسمى [id]، ثم إنشاء مسار جديد يسمى edit مع ملف page.tsx. يجب أن يبدو هيكل ملفك كما يلي:

مجلد الفواتير مع مجلد متداخل [id]، ومجلد edit بداخله

في مكون <Table> الخاص بك، لاحظ أن هناك زر <UpdateInvoice /> يتلقى معرّف الفاتورة id من سجلات الجدول.

/app/ui/invoices/table.tsx
export default async function InvoicesTable({
  query,
  currentPage,
}: {
  query: string;
  currentPage: number;
}) {
  return (
    // ...
    <td className="flex justify-end gap-2 whitespace-nowrap px-6 py-4 text-sm">
      <UpdateInvoice id={invoice.id} />
      <DeleteInvoice id={invoice.id} />
    </td>
    // ...
  );
}

انتقل إلى مكون <UpdateInvoice /> الخاص بك، وقم بتحديث href الخاص بـ Link لقبول خاصية id. يمكنك استخدام القوالب الحرفية للربط بمقطع مسار ديناميكي:

/app/ui/invoices/buttons.tsx
import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/outline';
import Link from 'next/link';
 
// ...
 
export function UpdateInvoice({ id }: { id: string }) {
  return (
    <Link
      href={`/dashboard/invoices/${id}/edit`}
      className="rounded-md border p-2 hover:bg-gray-100"
    >
      <PencilIcon className="w-5" />
    </Link>
  );
}

2. قراءة معرّف الفاتورة id من معلمات الصفحة params

عد إلى مكون <Page> الخاص بك، والصق الكود التالي:

/app/dashboard/invoices/[id]/edit/page.tsx
import Form from '@/app/ui/invoices/edit-form';
import Breadcrumbs from '@/app/ui/invoices/breadcrumbs';
import { fetchCustomers } from '@/app/lib/data';
 
export default async function Page() {
  return (
    <main>
      <Breadcrumbs
        breadcrumbs={[
          { label: 'Invoices', href: '/dashboard/invoices' },
          {
            label: 'Edit Invoice',
            href: `/dashboard/invoices/${id}/edit`,
            active: true,
          },
        ]}
      />
      <Form invoice={invoice} customers={customers} />
    </main>
  );
}

لاحظ كيف أنه مشابه لصفحة إنشاء الفاتورة /create، إلا أنه يستورد نموذجًا مختلفًا (من ملف edit-form.tsx). يجب أن يكون هذا النموذج مُعبأ مسبقًا بقيمة افتراضية لاسم العميل، مبلغ الفاتورة، والحالة. لتعبيئة حقول النموذج مسبقًا، تحتاج إلى جلب الفاتورة المحددة باستخدام id.

بالإضافة إلى searchParams، تقبل مكونات الصفحة خاصية تسمى params يمكنك استخدامها للوصول إلى id. قم بتحديث مكون <Page> الخاص بك لاستقبال الخاصية:

/app/dashboard/invoices/[id]/edit/page.tsx
import Form from '@/app/ui/invoices/edit-form';
import Breadcrumbs from '@/app/ui/invoices/breadcrumbs';
import { fetchCustomers } from '@/app/lib/data';
 
export default async function Page(props: { params: Promise<{ id: string }> }) {
  const params = await props.params;
  const id = params.id;
  // ...
}

3. جلب الفاتورة المحددة

ثم:

  • قم باستيراد دالة جديدة تسمى fetchInvoiceById ومرر id كوسيطة.
  • قم باستيراد fetchCustomers لجلب أسماء العملاء للقائمة المنسدلة.

يمكنك استخدام Promise.all لجلب كل من الفاتورة والعملاء في نفس الوقت:

/dashboard/invoices/[id]/edit/page.tsx
import Form from '@/app/ui/invoices/edit-form';
import Breadcrumbs from '@/app/ui/invoices/breadcrumbs';
import { fetchInvoiceById, fetchCustomers } from '@/app/lib/data';
 
export default async function Page(props: { params: Promise<{ id: string }> }) {
  const params = await props.params;
  const id = params.id;
  const [invoice, customers] = await Promise.all([
    fetchInvoiceById(id),
    fetchCustomers(),
  ]);
  // ...
}

سترى خطأً مؤقتًا في TypeScript لخاصية invoice في طرفيتك لأن invoice يمكن أن تكون undefined محتملة. لا تقلق بشأن ذلك الآن، ستقوم بحله في الفصل التالي عند إضافة معالجة الأخطاء.

رائع! الآن، اختبر أن كل شيء متصل بشكل صحيح. قم بزيارة http://localhost:3000/dashboard/invoices وانقر على أيقونة القلم لتعديل فاتورة. بعد التنقل، يجب أن ترى نموذجًا معبأً مسبقًا بتفاصيل الفاتورة:

صفحة تعديل الفواتير مع مسار التنقل والنموذج

يجب أيضًا تحديث عنوان URL بمعرّف id كما يلي: http://localhost:3000/dashboard/invoice/uuid/edit

UUIDs مقابل المفاتيح التلقائية المتزايدة

نستخدم UUIDs بدلاً من المفاتيح المتزايدة (مثل 1، 2، 3، إلخ). هذا يجعل عنوان URL أطول؛ ومع ذلك، تقضي UUIDs على خطر تصادم المعرّفات، فهي فريدة عالميًا، وتقلل من خطر هجمات التعداد - مما يجعلها مثالية لقواعد البيانات الكبيرة.

ومع ذلك، إذا كنت تفضل عناوين URL أنظف، فقد تفضل استخدام المفاتيح التلقائية المتزايدة.

4. تمرير id إلى إجراء الخادم

أخيرًا، تريد تمرير id إلى إجراء الخادم حتى تتمكن من تحديث السجل الصحيح في قاعدة البيانات الخاصة بك. لا يمكنك **تمرير id كوسيطة مثل هذا:

/app/ui/invoices/edit-form.tsx
// تمرير id كوسيطة لن يعمل
<form action={updateInvoice(id)}>

بدلاً من ذلك، يمكنك تمرير id إلى إجراء الخادم باستخدام bind في جافا سكريبت. سيضمن هذا تشفير أي قيم يتم تمريرها إلى إجراء الخادم.

/app/ui/invoices/edit-form.tsx
// ...
import { updateInvoice } from '@/app/lib/actions';
 
export default function EditInvoiceForm({
  invoice,
  customers,
}: {
  invoice: InvoiceForm;
  customers: CustomerField[];
}) {
  const updateInvoiceWithId = updateInvoice.bind(null, invoice.id);
 
  return <form action={updateInvoiceWithId}>{/* ... */}</form>;
}

ملاحظة: استخدام حقل إدخال مخفي في النموذج الخاص بك يعمل أيضًا (مثل <input type="hidden" name="id" value={invoice.id} />). ومع ذلك، ستظهر القيم كنص كامل في مصدر HTML، وهو ليس مثاليًا للبيانات الحساسة.

ثم، في ملف actions.ts الخاص بك، قم بإنشاء إجراء جديد، updateInvoice:

/app/lib/actions.ts
// استخدم Zod لتحديث الأنواع المتوقعة
const UpdateInvoice = FormSchema.omit({ id: true, date: true });
 
// ...
 
export async function updateInvoice(id: string, formData: FormData) {
  const { customerId, amount, status } = UpdateInvoice.parse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });
 
  const amountInCents = amount * 100;
 
  await sql`
    UPDATE invoices
    SET customer_id = ${customerId}, amount = ${amountInCents}, status = ${status}
    WHERE id = ${id}
  `;
 
  revalidatePath('/dashboard/invoices');
  redirect('/dashboard/invoices');
}

بشكل مشابه لإجراء createInvoice، هنا تقوم بما يلي:

  1. استخراج البيانات من formData.
  2. التحقق من الأنواع باستخدام Zod.
  3. تحويل المبلغ إلى سنتات.
  4. تمرير المتغيرات إلى استعلام SQL الخاص بك.
  5. استدعاء revalidatePath لمسح ذاكرة تخزين العميل وإجراء طلب خادم جديد.
  6. استدعاء redirect لإعادة توجيه المستخدم إلى صفحة الفواتير.

اختبر ذلك عن طريق تعديل فاتورة. بعد إرسال النموذج، يجب أن يتم إعادة توجيهك إلى صفحة الفواتير، ويجب تحديث الفاتورة.

حذف فاتورة

لحذف فاتورة باستخدام إجراء خادم، قم بلف زر الحذف في عنصر <form> ومرر id إلى إجراء الخادم باستخدام bind:

/app/ui/invoices/buttons.tsx
import { deleteInvoice } from '@/app/lib/actions';
 
// ...
 
export function DeleteInvoice({ id }: { id: string }) {
  const deleteInvoiceWithId = deleteInvoice.bind(null, id);
 
  return (
    <form action={deleteInvoiceWithId}>
      <button type="submit" className="rounded-md border p-2 hover:bg-gray-100">
        <span className="sr-only">Delete</span>
        <TrashIcon className="w-4" />
      </button>
    </form>
  );
}

داخل ملف actions.ts الخاص بك، قم بإنشاء إجراء جديد يسمى deleteInvoice.

/app/lib/actions.ts
export async function deleteInvoice(id: string) {
  await sql`DELETE FROM invoices WHERE id = ${id}`;
  revalidatePath('/dashboard/invoices');
}

نظرًا لأن هذا الإجراء يتم استدعاؤه في مسار /dashboard/invoices، لا تحتاج إلى استدعاء redirect. سيؤدي استدعاء revalidatePath إلى تشغيل طلب خادم جديد وإعادة عرض الجدول.

قراءة إضافية

في هذا الفصل، تعلمت كيفية استخدام إجراءات الخادم (Server Actions) لتعديل البيانات. كما تعلمت كيفية استخدام واجهة برمجة التطبيقات revalidatePath لإعادة التحقق من ذاكرة التخزين المؤقت لـ Next.js و redirect لإعادة توجيه المستخدم إلى صفحة جديدة.

يمكنك أيضًا قراءة المزيد حول الأمان مع إجراءات الخادم (security with Server Actions) لمزيد من التعلم.