Server Actions في Next.js — أداء خارق بدون API
Next.js4 دقيقة للقراءة

Server Actions في Next.js — أداء خارق بدون API

تعلّم كيف تستخدم Server Actions لإنشاء mutations آمنة وسريعة بدون كتابة API routes، مع progressive enhancement تلقائي.

ما هي Server Actions؟

Server Actions هي دوال تعمل على الخادم وتستطيع استدعاؤها مباشرة من المكوّن — بدون كتابة API route، وبدون fetch.

TSX
// قبل Server Actions
async function handleSubmit(data: FormData) {
  const res = await fetch("/api/posts", {
    method: "POST",
    body: data,
  });
  // ...
}
 
// مع Server Actions
async function createPost(data: FormData) {
  "use server";
  await db.post.create({ data: { title: data.get("title") as string } });
  revalidatePath("/blog");
}

كلتاهما تنفذ نفس الشيء، لكن الثانية أبسط وأكثر أمانًا.


كيف تعمل؟

عند وضع "use server" في دالة، يحوّلها Next.js إلى endpoint مشفّر تلقائيًا. المتصفح يستدعي هذا الـ endpoint بدون أن تكتب API route.

Text
المتصفح → يستدعي الدالة → Next.js يُرسل POST إلى endpoint مشفّر → الخادم ينفّذ الكود → يُرجع النتيجة

لا يمكن للمتصفح رؤية كود الـ Server Action أبدًا. فقط اسم الـ endpoint المُشفَّر يُرسَل.


الاستخدام مع النماذج

أبسط مثال

TSX
// app/blog/new/page.tsx
export default function NewPostPage() {
  async function createPost(formData: FormData) {
    "use server";
 
    const title = formData.get("title") as string;
    const content = formData.get("content") as string;
 
    await db.post.create({ data: { title, content } });
    redirect("/blog");
  }
 
  return (
    <form action={createPost}>
      <input name="title" placeholder="العنوان" required />
      <textarea name="content" placeholder="المحتوى" required />
      <button type="submit">نشر</button>
    </form>
  );
}

يعمل هذا حتى بدون JavaScript في المتصفح — هذا هو الـ progressive enhancement.


مع useActionState

لإضافة حالة التحميل والأخطاء:

TSX
"use client";
 
import { useActionState } from "react";
import { createPost } from "./actions";
 
type State = { error?: string; success?: boolean };
 
export function NewPostForm() {
  const [state, action, isPending] = useActionState<State, FormData>(
    createPost,
    {}
  );
 
  return (
    <form action={action}>
      {state.error && (
        <p className="text-red-600">{state.error}</p>
      )}
      {state.success && (
        <p className="text-green-600">تم النشر بنجاح!</p>
      )}
 
      <input name="title" placeholder="العنوان" required />
 
      <button disabled={isPending}>
        {isPending ? "جارٍ النشر..." : "نشر المقال"}
      </button>
    </form>
  );
}
TypeScript
// actions.ts
"use server";
 
export async function createPost(
  _prevState: { error?: string },
  formData: FormData
) {
  const title = formData.get("title") as string;
 
  if (title.length < 5) {
    return { error: "العنوان يجب أن يكون 5 أحرف على الأقل" };
  }
 
  try {
    await db.post.create({ data: { title } });
    revalidatePath("/blog");
    return { success: true };
  } catch {
    return { error: "فشل النشر، حاول مجددًا" };
  }
}

Optimistic Updates

تحديث الواجهة فورًا قبل انتهاء العملية على الخادم:

TSX
"use client";
 
import { useOptimistic, useTransition } from "react";
import { toggleLike } from "./actions";
 
export function LikeButton({ postId, initialLikes }: Props) {
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    initialLikes,
    (state, increment: number) => state + increment
  );
  const [isPending, startTransition] = useTransition();
 
  const handleLike = () => {
    startTransition(async () => {
      addOptimisticLike(1); // فوري في الواجهة
      await toggleLike(postId); // يعمل في الخلفية
    });
  };
 
  return (
    <button onClick={handleLike} disabled={isPending}>
      ❤️ {optimisticLikes}
    </button>
  );
}

الأمان

التحقق من المصادقة

TypeScript
"use server";
 
import { auth } from "@/lib/auth";
 
export async function deletePost(postId: string) {
  // تحقق دائمًا من المستخدم داخل الـ action
  const session = await auth();
  if (!session?.user) throw new Error("غير مصرح");
 
  // تحقق من الصلاحيات
  const post = await db.post.findUnique({ where: { id: postId } });
  if (post?.authorId !== session.user.id) throw new Error("غير مصرح");
 
  await db.post.delete({ where: { id: postId } });
  revalidatePath("/blog");
}

لا تثق أبدًا في المدخلات القادمة من المتصفح. تحقق من الهوية والصلاحيات داخل كل Server Action بشكل مستقل.


متى تستخدم Server Actions؟

الحالةالأنسب
Forms بسيطةServer Action مباشر
Real-time, WebSocketAPI Route
External webhookAPI Route
CRUD عاديServer Action
File upload كبيرAPI Route مع streaming
Optimistic UIServer Action + useOptimistic

الخلاصة

Server Actions تُبسّط كثيرًا من كود الـ full-stack — لا fetch، لا API routes، لا CORS. الكود أقل، والأمان أفضل لأن المنطق يعمل فقط على الخادم.