Server Actions في Next.js: نموذج يعمل قبل أن تُحمَّل جافاسكريبت
Server Actions في Next.js: نموذج يعمل قبل أن تُحمَّل جافاسكريبت
النمط الكلاسيكي لنموذج في React: onSubmit، وevent.preventDefault()، وfetch('/api/todos', { method: 'POST', body: JSON.stringify(...) })، وuseState لإدارة حالة التحميل. يعمل هذا – بشرط واحد يسهل نسيانه، لأنه لا يظهر أبدًا على حاسوبك المحمول الخاص بوصلة إنترنت سريعة: يجب أن تتاح فرصة لجافاسكريبت لتُحمَّل، وتُحلَّل، وتُنفَّذ، قبل أن يبدأ النموذج أصلًا الاستجابة للنقرة. على وصلة جوّالة بطيئة، أثناء ترطيب (hydration) صفحة كبيرة، أو حين يحجب امتداد متصفح السكربت – يضغط المستخدم «إرسال»، ولا يحدث شيء حرفيًّا. لا خطأ يظهر، ولا مؤشّر تحميل – فـonClick ببساطة لم يوجد بعد.
غالبًا ما تُقدَّم Server Actions في Next.js على أنها صياغة أكثر راحة للاستدعاء نفسه تمامًا – «fetch، لكن دون الحاجة لكتابة مسار API». هذا تبسيط يُغفِل ما هو جديد فعليًّا في هذه الآلية: <form action={myAction}> يعتمد على آلية أصيلة، موجودة منذ الأزل، لإرسال النماذج عبر المتصفح، لا على جافاسكريبت كشرط ضروري. هذا يُغيِّر كلًّا من طريقة عمل الآلية تحت السطح والأخطاء التي ترتكبها الفرق فعليًّا حين تتعامل مع "use server" كاستدعاء عادي لدالة محلّية.
ما الذي يفعله توجيه "use server" فعليًّا
حين تُعلِّم دالة بـ"use server"، لا يترك مُصرِّف Next.js جسدها في الحزمة التي يتلقّاها المتصفح. بل يقصّه ويستبدله بـمرجع – معرِّف مُشفَّر يشير إلى الدالة الواجب استدعاؤها على الخادم. ما يصل فعليًّا إلى العميل هو بديل صغير (stub): «حين تُستدعى، أرسل طلب POST إلى نقطة نهاية RSC خاصة بهذا المعرِّف مع الوسائط المُسلسَلة (serialized)». المنطق نفسه – استعلام قاعدة البيانات، والتحقّق من الصحّة، أو أيّ شيء تفعله الدالة – لا يغادر الخادم فعليًّا أبدًا.
tsx
// app/actions.ts"use server";import { revalidatePath } from "next/cache";import { db } from "@/lib/db";export async function addTodoAction(prevState: unknown, formData: FormData) {const title = formData.get("title");if (typeof title !== "string" || title.trim().length === 0) {return { error: "العنوان لا يمكن أن يكون فارغًا." };}await db.todo.create({ data: { title: title.trim() } });revalidatePath("/todos");return { error: null };}
حين تُلحق هذه الدالة بـ<form action={addTodoAction}>، يحدث أمر لم يكن fetch العادي يمنحه أبدًا مجّانًا: يعمل هذا قبل أن تُنفَّذ أيّ جافاسكريبت إطلاقًا. المتصفح قادر منذ الأزل على إرسال نموذج بوصفه POST أصيلًا إن كانت سمة action تشير إلى رابط – وتُوسِّع React 19 وNext.js هذا بحيث يمكن لـaction أن تكون أيضًا دالة خادمية، ويُنفِّذ المتصفح مساره الأصيل على أيّ حال إن لم تكن JS جاهزة بعد. حين تعمل جافاسكريبت بالفعل، يعترض Next.js هذا الحدث ويُنفِّذ الأمر نفسه عبر fetch في الخلفية، دون إعادة تحميل الصفحة، مع تحديث سلس لواجهة المستخدم. هذا تحسين تدريجي مدمج في الإطار (progressive enhancement)، لا شيئًا عليك برمجته بنفسك – الآلية نفسها بالضبط التي كتبنا عنها بمناسبة RouteAnnouncer في مقال إتاحة الوصول، لكن مطبَّقة هنا على تعديل البيانات بدلًا من التنقّل.
مثال عملي: نموذج بإدارة حالة كاملة دون fetch يدوي
أضاف React 19 خطاف useActionState خصّيصًا لهذا النمط – يربط استدعاء الدالة الخادمية بالحالة المُعادة من آخر تنفيذ لها، دون الحاجة لـuseState يدوي لتخزين الخطأ أو حالة التحميل.
tsx
"use client";import { useActionState } from "react";import { addTodoAction } from "./actions";const initialState = { error: null };const TodoForm = () => {const [state, formAction, isPending] = useActionState(addTodoAction,initialState,);return (<form action={formAction}><input type="text" name="title" placeholder="مهمّة جديدة" required /><button type="submit" disabled={isPending}>{isPending ? "جارٍ الإضافة…" : "أضف"}</button>{state.error && <p className="form-error">{state.error}</p>}</form>);};export default TodoForm;
عدّة أمور تحدث هنا في آنٍ واحد، دون سطر واحد مخصَّص لإدارة الطلب يدويًّا. يعكس isPending الحالة الفعلية لإرسال النموذج – بما في ذلك ذلك الطلب الأصيل السابق للترطيب، لا استدعاءات جانب العميل وحدها. state.error هو مباشرة ما أعادته الدالة الخادمية – دون أيّ تحليل لاستجابة JSON، ودون try/catch حول fetch. أمّا revalidatePath("/todos") داخل الدالة فيُلزم Next.js بتحديث الذاكرة المؤقتة (cache) الخاصة بذلك المسار، فتُظهر قائمة المهام في الصفحة الإدخال الجديد دون تحديث يدوي لحالة العميل – الإطار، لا أنت، من يقرّر متى وكيف يُحدَّث حمولة RSC الجديدة.
الفخاخ والممارسات الجيدة
- كلّ دالة
"use server"هي نقطة نهاية HTTP عامة – لا دالة خاصّة. هذا الخطأ الأمني الأكثر شيوعًا في Server Actions: بما أنّ لا شيء في واجهة المستخدم يربطها صراحةً، تبدو «مخفيّة». إنها ليست كذلك – يُنشئ المُصرِّف لها معرِّف إجراء دائمًا وقابلًا للاستدعاء من الخارج، فأيّ شخص يعرفه (مثلًا عبر مراقبة حركة الشبكة في أدوات المطوّرين) يستطيع استدعاءها مباشرة، متجاوزًا واجهة المستخدم لديك وافتراضاتها. يجب تنفيذ التحقّق من صحّة المدخلات والتحقّق من الصلاحيات (الجلسة، دور المستخدم) داخل الدالة نفسها، تمامًا كما في نقطة نهاية API كلاسيكية – لا تعتمد أبدًا على أنّ «الزرّ غير مرئيّ لغير المسجَّلين». - يجب أن تكون الوسائط والقيمة المُعادة قابلة للتسلسل (serializable). تُرسِل Server Actions البيانات عبر الشبكة بتنسيق تسلسل React (أغنى من JSON – يدعم مثلًا
DateوMapوكائنFormDataالمُمرَّر مباشرة من النموذج)، لكنها لا تزال لا تسمح بمرور الدوال أو نُسخ الأصناف أو مراجع عناصر DOM. إن استخدمت.bind()لتضمين وسيط إضافي عند الاستدعاء من العميل (addTodoAction.bind(null, listId))، تذكّر أنّlistIdأيضًا يجب أن يكون قابلًا للتسلسل. - لا يزال هذا طلبًا شبكيًّا حقيقيًّا، لا استدعاء دالة محلّي. يسهل نسيان ذلك، لأنّ
await myAction(data)يبدو نحويًّا مطابقًا تمامًا لاستدعاء دالة جافاسكريبت عادية. تحت السطح، إنه دائمًا رحلة كاملة ذهابًا وإيابًا: تسلسل، وHTTP، وفكّ تسلسل على الطرف الآخر. استدعاء Server Action ضمن حلقة عند كلّ حرف يُكتَب في حقل نصّي (مثلًا للتحقّق الفوري من الصحّة) يحوّل نموذجك إلى مولّد طلبات، لا إلى حقل استجابة – في مثل هذه الحالات، التأخير المتحكَّم فيه (debounce) ضروري بقدر ضرورته في أيّ استدعاء شبكي آخر. - غياب
revalidatePath/revalidateTagيترك واجهة المستخدم بذاكرة مؤقتة قديمة. يخزّن Next.js افتراضيًّا بيانات الصفحات مؤقتًا – فإجراء يحفظ شيئًا في قاعدة البيانات دون إخبار الإطار بأيّ مسار يجب تحديثه ينتج عنه غالبًا شكوى «حُفِظ لكن لا يظهر» – البيانات صحيحة في قاعدة البيانات، لكنّ الواجهة لا تزال تُظهر نسخة قديمة مخزَّنة مؤقتًا من الصفحة.
الخلاصة
Server Actions ليست «fetch بصياغة أجمل» – إنهما شيئان مختلفان مبنيّان على الآلية نفسها. أوّلًا، تعتمد على إرسال النماذج الأصيل عبر المتصفح، فيعمل النموذج حتى قبل أن تُحمَّل جافاسكريبت – دون أن تكتب أنت سطرًا واحدًا مسؤولًا عن هذا السلوك، تحصل عليه من بنية <form action={...}> نفسها. ثانيًا، والأهمّ من منظور الأمان: كلّ دالة موسومة بـ"use server" تصبح نقطة نهاية حقيقية وقابلة للاستدعاء من الخارج على الشبكة، بصرف النظر عمّا إذا كان أيّ عنصر مرئيّ في واجهة المستخدم يستدعيها. التعامل معها كدالة محلّية وموثوقة – دون تحقّق وتفويض بداخلها – هو أبسط طريق لتحويل صياغة مريحة إلى نقطة نهاية API غير محمية.