Skip to main content
English
العودة إلى المدوّنة

تقنية

نظرة معمّقة في Server Actions في Next.js

كيف تبسّط Server Actions في Next.js تعديل البيانات بإلغاء الحاجة إلى مسارات API، وكيف تدير التواصل بين العميل والخادم بسلاسة.

2 أغسطس 20263 دقائق قراءة

اقرأ هذه المقالة بالإنكليزية

نظرة معمّقة في Server Actions في Next.js

مشكلة النماذج التقليدية

لسنوات، كان إرسال نموذج يعني كتابة مسار API، وتوصيل fetch، وإدارة حالتَي التحميل والخطأ يدوياً، ثم اكتشاف CORS. النموذج في ملف، ونقطة النهاية في ملف آخر، والرابط الوحيد بينهما سلسلة نصية لم يتحقّق منها أي مترجم يوماً.

و Server Actions تطوي ذلك كلّه. تكتب دالّة واحدة، وتعلّمها بأنّها تعمل على الخادم، وتسلّمها مباشرةً إلى <form>. ويتكفّل Next.js بالشبكة بينهما.

ما هو Server Action فعلاً

‏Server Action دالّة لا تعمل إلا على الخادم، وإن كنت تشير إليها من كود العميل. وتوجيه "use server" هو ما يعلّمها:

// src/actions/contact.ts
"use server";

import { z } from "zod";
import { revalidatePath } from "next/cache";
import { prisma } from "@/lib/db/client";

const Schema = z.object({
  email: z.string().email(),
  message: z.string().min(10).max(5000),
});

export async function submitContact(formData: FormData) {
  const parsed = Schema.safeParse({
    email: formData.get("email"),
    message: formData.get("message"),
  });

  if (!parsed.success) {
    return { ok: false, error: "Please check the form and try again." };
  }

  await prisma.contactSubmission.create({ data: parsed.data });
  revalidatePath("/admin/messages");

  return { ok: true, error: null };
}

والنموذج نفسه لا يحتاج حالة، ولا معالجاً، ولا useEffect:

import { submitContact } from "@/actions/contact";

export default function ContactPage() {
  return (
    <form action={submitContact}>
      <input name="email" type="email" required />
      <textarea name="message" required />
      <button type="submit">Send</button>
    </form>
  );
}

هذه هي الرحلة كاملةً. لا مسار API، ولا fetch، ولا تحويل إلى JSON كتبته بنفسك.

يعمل قبل أن يُحمَّل JavaScript

هذا هو الجزء الذي لا يُعطى حقّه. تمرير Server Action إلى action ينتج نموذجاً يُرسَل فعلاً دون JavaScript على العميل — إذ يولّد Next.js هدف POST حقيقياً ثم يحسّنه تدريجياً بعد اكتمال الترطيب.

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

قراءة النتيجة

تتغيّر البصمة حين تريد عرض النتيجة. فـ useActionState تمرّر الحالة السابقة وسيطاً أول، فتصير الدالّة (prevState, formData):

"use client";

import { useActionState } from "react";
import { submitContact } from "@/actions/contact";

const initial = { ok: false, error: null };

export function ContactForm() {
  const [state, formAction, pending] = useActionState(submitContact, initial);

  return (
    <form action={formAction}>
      <input name="email" type="email" required />
      <textarea name="message" required />
      <button type="submit" disabled={pending}>
        {pending ? "Sending…" : "Send"}
      </button>
      {state.error && <p role="alert">{state.error}</p>}
    </form>
  );
}

والخلط بين البصمتين أكثر أخطاء Server Actions شيوعاً ممّا أراه: دالّة كُتبت لـ action={fn} ثم مُرّرت إلى useActionState تستقبل FormData في الوسيط الثاني، فتُقرأ كل الحقول null بصمت.

كل دالّة نقطة نهاية عامّة

هذا أهمّ من كل ما سبق من راحة في الكتابة، وهو ما تتخطّاه الشروح.

تُترجَم أي دالّة مصدَّرة بـ "use server" إلى نقطة نهاية HTTP قابلة للاستدعاء. وأي شخص يستطيع استدعاءها بطلب مصنوع خصيصاً. ولا يهمّ أنّ واجهتك لا تعرض الزرّ إلا للمشرفين المسجّلين — فالزرّ ليس حدّ الأمان، بل الدالّة هي الحدّ.

لذا تتحقّق كل دالّة تعدّل شيئاً من الصلاحية بنفسها، أولاً:

"use server";

export async function deleteProject(id: string) {
  const adminId = await getAdminId();
  if (!adminId) throw new Error("Unauthorised");

  await prisma.project.delete({ where: { id } });
  revalidatePath("/admin/projects");
}

والأمر نفسه ينطبق على التحقّق من المدخلات. فـ required على حقل إدخال تلميح للمتصفّح، لا قيد على الطلب. تحقّق من كل شيء على الخادم — وsafeParse أعلاه ليست مراسم، بل هي التحقّق الوحيد الذي يصمد فعلاً.

متى لا تستعملها

‏Server Actions للتعديلات. تعمل بالتسلسل، وليست طبقة عامّة لجلب البيانات:

  • تقرأ بيانات؟ اجلبها في Server Component. فهو على الخادم أصلاً ولا يحتاج دالّة إطلاقاً.
  • شيء يستدعيه طرف ثالث؟ هذا Route Handler بمصادقته الخاصة، لا Server Action.
  • عمل طويل؟ الدالّة تُبقي الطلب مفتوحاً. ضعه في طابور بدل ذلك.

ماذا يعني هذا إن كنت تدفع ثمن موقع

لا شيء ممّا سبق يفرض عليك الاهتمام بـ React. وما يشتريه لك ملموس: أجزاء متحرّكة أقلّ قابلة للعطب، ونماذج تعمل حتى والصفحة ما زالت تُحمَّل، وتحقّق من المدخلات لا يمكن تخطّيه بإطفاء JavaScript، ولوحة تحكّم مبنية من الكود نفسه الذي بُني منه الموقع العام لا مركّبة بجانبه.

وهذه هي البنية التي أبني عليها كل موقع لعميل، ولهذا يبدأ عملي بـ Next.js في شمال لبنان من App Router لا من قالب جاهز. وإن كنت تزن إعادة بناء، ففي صفحة المنية دليل اختيار المطوّر بالأسئلة التي تستحقّ أن تطرحها على من توظّفه أيّاً كان.