Next.js: أساسيات Server Actions
آخر تحديث: 2026-08-26
Server Actions هي "القوة الخارقة" لـ Next.js — إنها تسمح للمتصفح باستدعاء دوال جانب الخادم مباشرة، دون الحاجة إلى بناء نقاط نهاية API يدوياً.
1. ما ستتعلمه
- كيفية تعريف أمر
'use server'و Server Actions - وضع معالجة النماذج في
<form action={}> - التحسين التدريجي: يمكن إرسال النماذج حتى بدون JavaScript
revalidatePath()لتحديث بيانات الصفحة بعد الإرسال- دمج Zod للتحقق من صحة معاملات النموذج
2. قصة حقيقية لمطور Full-Stack
(1) نقطة الألم: يستغرق 80 سطراً من الكود لإرسال نموذج "بسيط"
يقوم بوب بإضافة ميزة "إنشاء مشروع" إلى TaskFlow. يتطلب النهج التقليدي:
- كتابة مسار API
POST /api/projects(20 سطراً) - كتابة استدعاء
fetch()من جانب العميل (10 أسطر) - معالجة رمز CSRF (10 أسطر)
- معالجة حالات التحميل والخطأ والنجاح (20 سطراً)
- تحديث بيانات الصفحة (10 أسطر)
- التحقق من صحة الإدخال (10 أسطر)
- الإجمالي: حوالي 80 سطراً من الكود
تنهد بوب قائلاً: "أنا فقط أريد إرسال نموذج."
(2) حل Server Actions
معالجة النماذج مباشرة باستخدام دالة
'use server'واحدة — بدون عبء JavaScript من جانب العميل، بدون مسارات API.
// app/projects/page.tsx
export default function ProjectsPage() {
return (
<form action={async (formData: FormData) => {
'use server'
const name = formData.get('name') as string
await db.project.create({ data: { name } })
revalidatePath('/projects')
}}>
<input name="name" required placeholder="Project name" />
<button type="submit">Create Project</button>
</form>
)
}
(3) العائد
| البُعد | مسار API التقليدي | Server Actions |
|---|---|---|
| عدد أسطر الكود | 80 سطراً | 15 سطراً |
| ملف مسار API | ✅ ملفات إضافية مطلوبة | ❌ غير مطلوب |
| اعتماديات JavaScript | ✅ مطلوبة | ❌ تحسين تدريجي |
| حماية CSRF | ❌ يدوية | ✅ مضمنة |
| حالة التحميل | ✅ حالة يدوية مطلوبة | ✅ useActionState |
| أمان الأنواع | ❌ فضفاض | ✅ أنواع TS كاملة |
3. سير العمل الأساسي لـ Server Actions
sequenceDiagram
participant Browser as المتصفح
participant RSC as RSC Payload
participant SA as Server Action
participant DB as قاعدة البيانات
Browser->>SA: <form action={action}> إرسال
SA->>SA: علامات وقت التجميع 'use server'
SA->>DB: كتابة مباشرة لقاعدة البيانات
DB-->>SA: نجاح
SA->>SA: revalidatePath() / revalidateTag()
SA-->>Browser: إرجاع RSC Payload (واجهة مستخدم جديدة)
Note over Browser: لا حاجة لتحديث الصفحة بالكامل
| المشارك | الدور | الوصف |
|---|---|---|
| المتصفح | مرسل النموذج | يُستدعى عبر <form action> أو JS |
| RSC Payload | بروتوكول النقل | جسر تسلسلي بين المتصفح والخادم |
| Server Action | المعالج | دالة غير متزامنة موسومة بـ 'use server' |
| قاعدة البيانات | استمرارية البيانات | Prisma / Drizzle أو API خارجي |
4. ثلاث طرق لتعريف Server Actions
لدى Server Action موقعان للتعريف وتكامل واحد مع API الخاص بـ React 19:
| الطريقة | الموقع | حالة الاستخدام | مثال الكود |
|---|---|---|---|
| ملف منفصل | app/actions.ts |
مشاركة عبر صفحات متعددة | export async function createProject(...) |
| مضمنة | داخل مكون <form action={async ...}> |
عملية بسيطة | 'use server' داخل دالة |
| useActionState | مكون عميل | تتطلب إدارة حالة معقدة | useActionState(action, initialState) |
(1) وضع الملف المنفصل (موصى به)
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
import { db } from '@/lib/db'
export async function createProject(formData: FormData) {
const name = formData.get('name') as string
const description = formData.get('description') as string
await db.project.create({
data: { name, description }
})
revalidatePath('/projects')
}
export async function deleteProject(id: number) {
await db.project.delete({ where: { id } })
revalidatePath('/projects')
}
// app/projects/page.tsx
import { createProject } from '@/app/actions'
export default function ProjectsPage() {
return (
<form action={createProject}>
<input name="name" required />
<input name="description" />
<button type="submit">Create</button>
</form>
)
}
(2) الوضع المضمن (السيناريوهات البسيطة)
// app/inline-demo/page.tsx — Server Action مضمنة
export default function InlineDemoPage() {
return (
<form action={async (formData: FormData) => {
'use server'
const message = formData.get('message') as string
console.log('Received:', message)
// معالجة مباشرة، لا حاجة لملفات إضافية
}}>
<input name="message" required />
<button type="submit">Send</button>
</form>
)
}
(3) نمط useActionState (موصى به للنماذج ذات الحالة)
// app/todos/use-action-state.tsx
'use client'
import { useActionState } from 'react'
async function addTodo(prevState: string[], formData: FormData) {
'use server'
const todo = formData.get('todo') as string
await db.todo.create({ data: { title: todo } })
revalidateTag('todos')
return [...prevState, todo]
}
export default function TodoForm() {
const [todos, formAction, isPending] = useActionState(addTodo, [])
return (
<form action={formAction}>
<input name="todo" required disabled={isPending} />
<button type="submit" disabled={isPending}>
{isPending ? 'Adding...' : 'Add Todo'}
</button>
<ul>{todos.map((t, i) => <li key={i}>{t}</li>)}</ul>
</form>
)
}
▶ مثال: مقارنة طرق التعريف الثلاث (مستوى الصعوبة: ⭐)
المخرجات:
نموذج يحتوي على حقول: todo.
عند الإرسال، يقوم Server Action بمعالجة البيانات وإبطال وسوم التخزين المؤقت ذات الصلة.
// app/actions-comparison/page.tsx
import { createTodoInline, createTodoAction } from './actions'
export default function ActionsComparisonPage() {
return (
<div>
<h1>Server Actions — 3 طرق</h1>
{/* الطريقة 1: ملف منفصل */}
<section>
<h2>1. ملف منفصل</h2>
<form action={createTodoAction}>
<input name="title" required />
<button type="submit">Create</button>
</form>
</section>
{/* الطريقة 2: مضمنة */}
<section>
<h2>2. مضمنة</h2>
<form action={async (formData: FormData) => {
'use server'
const title = formData.get('title') as string
await fetch('https://jsonplaceholder.typicode.com/todos', {
method: 'POST', body: JSON.stringify({ title, completed: false })
})
}}>
<input name="title" required />
<button type="submit">Create</button>
</form>
</section>
{/* الطريقة 3: استدعاء مكون عميل */}
<section>
<h2>3. useActionState (انظر أدناه)</h2>
<TodoFormWithState />
</section>
</div>
)
}
المخرجات:
حقول النموذج: title. عند الإرسال، يعالج بيانات النموذج على الخادم.
المحتوى المرئي: Server Actions — 3 طرق | 1. ملف منفصل | Create
// app/actions-comparison/TodoFormWithState.tsx
'use client'
import { useActionState } from 'react'
import { createTodoAction } from './actions'
export default function TodoFormWithState() {
const [state, action, pending] = useActionState(createTodoAction, null)
return (
<form action={action}>
<input name="title" required disabled={pending} />
<button type="submit">{pending ? 'Creating...' : 'Create'}</button>
{state?.error && <p style={{ color: 'red' }}>{state.error}</p>}
{state?.success && <p style={{ color: 'green' }}>Todo created!</p>}
</form>
)
}
// app/actions-comparison/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
export async function createTodoAction(prevState: any, formData: FormData) {
const title = formData.get('title') as string
if (!title || title.length < 2) {
return { error: 'يجب أن يكون العنوان مكوناً من حرفين على الأقل' }
}
await fetch('https://jsonplaceholder.typicode.com/todos', {
method: 'POST', body: JSON.stringify({ title, completed: false })
})
revalidatePath('/actions-comparison')
return { success: true }
}
export async function createTodoInline(formData: FormData) {
const title = formData.get('title') as string
await fetch('https://jsonplaceholder.typicode.com/todos', {
method: 'POST', body: JSON.stringify({ title, completed: false })
})
revalidatePath('/actions-comparison')
}
5. خاصية "action" في النموذج والتحسين التدريجي
أهم ميزة في Server Actions هي التحسين التدريجي — حتى إذا تم تعطيل JavaScript في المتصفح، لا يزال من الممكن إرسال النماذج بنجاح.
(1) سلوك نموذج HTML الأصلي
// app/progressive/page.tsx — التحسين التدريجي: يعمل حتى عند تعطيل JS
export default function ProgressivePage() {
return (
<form action={async (formData: FormData) => {
'use server'
const email = formData.get('email') as string
const message = formData.get('message') as string
await sendEmail({ to: email, body: message })
revalidatePath('/progressive')
}}>
<label>البريد الإلكتروني: <input name="email" type="email" required /></label>
<label>الرسالة: <textarea name="message" required /></label>
<button type="submit">إرسال</button>
</form>
)
}
| الحالة | HTML الأصلي | + تحسينات JavaScript |
|---|---|---|
| طريقة الإرسال | POST إلى الرابط الحالي |
استخدام fetch + RSC Payload |
| تحديث الصفحة | تحديث الصفحة بالكامل | بدون تحديث كامل للصفحة (Soft Navigation) |
| تجربة المستخدم | إرسال نموذج تقليدي | إرسال بدون وميض |
| الميزة | ✅ متاحة بالكامل | ✅ تجربة محسنة |
(2) استخدام خاصية action مقابل onSubmit
| الطريقة | متطلبات HTML | متطلبات JS | التحسين التدريجي |
|---|---|---|---|
<form action={serverAction}> |
✅ لا حاجة لـ JS | ✅ محسّن | ✅ نعم |
<form onSubmit={handler}> |
❌ مطلوب preventDefault |
✅ مطلوب | ❌ لا |
▶ مثال: اختبار سيناريو تعطيل JS (مستوى الصعوبة ⭐⭐)
المخرجات:
نموذج يحتوي على حقول إدخال وزر إرسال.
عند الإرسال، يقوم Server Action بمعالجة البيانات وتحديث التخزين المؤقت للصفحة.
// app/no-js-demo/page.tsx — اختبار التحسين التدريجي
export default function NoJsDemoPage() {
return (
<form action={async (formData: FormData) => {
'use server'
const item = formData.get('item') as string
await fetch('https://jsonplaceholder.typicode.com/todos', {
method: 'POST', body: JSON.stringify({ title: item, completed: false })
})
revalidatePath('/no-js-demo')
}}>
<input name="item" required placeholder="أدخل عنصر المهمة" />
<button type="submit">إضافة مهمة</button>
</form>
)
}
المخرجات:
نموذج يحتوي على حقول إدخال وزر إرسال.
عند الإرسال، يقوم Server Action بمعالجة البيانات وتحديث التخزين المؤقت للصفحة.
طريقة الاختبار:
- الاستخدام العادي: أدخل نصاً → انقر "إرسال" → يتم تحديث القائمة
- تعطيل JavaScript: DevTools → الإعدادات → تعطيل JavaScript → تحديث → إرسال النموذج → لا يزال يعمل
6. دمج التحقق باستخدام Zod
يجب على Server Actions دائماً التحقق من صحة بيانات الإدخال. Zod هي مكتبة التحقق الأكثر شيوعاً في نظام TypeScript البيئي.
(1) وضع التحقق الأساسي
// app/actions/schema.ts
import { z } from 'zod'
export const projectSchema = z.object({
name: z.string().min(2, 'يجب أن يكون الاسم مكوناً من حرفين على الأقل').max(100),
description: z.string().max(500).optional(),
dueDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'تنسيق تاريخ غير صالح').optional(),
})
// app/actions/create.ts
'use server'
import { revalidatePath } from 'next/cache'
import { projectSchema } from './schema'
export async function createProject(prevState: any, formData: FormData) {
const validated = projectSchema.safeParse({
name: formData.get('name'),
description: formData.get('description'),
dueDate: formData.get('dueDate'),
})
if (!validated.success) {
return { errors: validated.error.flatten().fieldErrors }
}
try {
await db.project.create({ data: validated.data })
revalidatePath('/projects')
return { success: true }
} catch (err) {
return { error: 'فشل إنشاء المشروع' }
}
}
(2) عرض خطأ التحقق في العميل
// app/projects/CreateProjectForm.tsx
'use client'
import { useActionState } from 'react'
import { createProject } from '@/app/actions/create'
export function CreateProjectForm() {
const [state, action, pending] = useActionState(createProject, null)
return (
<form action={action}>
<div>
<input name="name" required placeholder="اسم المشروع" disabled={pending} />
{state?.errors?.name && <p style={{ color: 'red' }}>{state.errors.name[0]}</p>}
</div>
<div>
<textarea name="description" placeholder="الوصف" disabled={pending} />
</div>
<div>
<input name="dueDate" type="date" disabled={pending} />
{state?.errors?.dueDate && <p style={{ color: 'red' }}>{state.errors.dueDate[0]}</p>}
</div>
<button type="submit" disabled={pending}>
{pending ? 'جارٍ الإنشاء...' : 'إنشاء مشروع'}
</button>
{state?.success && <p style={{ color: 'green' }}>تم إنشاء المشروع!</p>}
{state?.error && <p style={{ color: 'red' }}>{state.error}</p>}
</form>
)
}
▶ مثال: نموذج تحقق Zod كامل (مستوى الصعوبة: ⭐⭐)
المخرجات:
نموذج مع تحديثات واجهة مستخدم متفائلة باستخدام useActionState.
النص المرئي: } | } | تم إنشاء المشروع! | }
{state?.error &&
// app/zod-demo/page.tsx — تحقق Zod + Server Actions
import { z } from 'zod'
import { revalidatePath } from 'next/cache'
const contactSchema = z.object({
name: z.string().min(2),
email: z.string().email('بريد إلكتروني غير صالح'),
message: z.string().min(10, 'الرسالة قصيرة جداً').max(1000),
})
export default function ZodDemoPage() {
return (
<form action={async (formData: FormData) => {
'use server'
const validated = contactSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
})
if (!validated.success) {
return { errors: validated.error.flatten().fieldErrors }
}
console.log('Contact form submitted:', validated.data)
revalidatePath('/zod-demo')
return { success: true }
}}>
<div style={{ marginBottom: 12 }}>
<label>الاسم: <input name="name" required /></label>
</div>
<div style={{ marginBottom: 12 }}>
<label>البريد الإلكتروني: <input name="email" type="email" required /></label>
</div>
<div style={{ marginBottom: 12 }}>
<label>الرسالة: <textarea name="message" required /></label>
</div>
<button type="submit">إرسال التواصل</button>
</form>
)
}
المخرجات:
حقول النموذج: name, email. عند الإرسال، يعالج البيانات ويحدث التخزين المؤقت للصفحة.
المحتوى المرئي: الاسم: | البريد الإلكتروني: | الرسالة:
▶ مثال: revalidatePath لتحديث القائمة (مستوى الصعوبة: ⭐)
المخرجات:
يعرض المكون واجهة المستخدم الموصوفة في المتصفح.
// app/todos/page.tsx — تحديث القائمة بعد الإرسال
import { revalidatePath } from 'next/cache'
export default async function TodosPage() {
const todos = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=5', {
next: { tags: ['todos'] }
}).then(r => r.json())
return (
<div>
<h1>المهام</h1>
<ul>{todos.map((t: any) => <li key={t.id}>{t.title}</li>)}</ul>
<form action={async (formData: FormData) => {
'use server'
const title = formData.get('title') as string
await fetch('https://jsonplaceholder.typicode.com/todos', {
method: 'POST', body: JSON.stringify({ title, completed: false })
})
revalidatePath('/todos')
}}>
<input name="title" required />
<button type="submit">إضافة</button>
</form>
</div>
)
}
المخرجات:
نموذج يحتوي على حقول إدخال وزر إرسال.
عند الإرسال، يقوم Server Action بمعالجة البيانات وتحديث التخزين المؤقت للصفحة.
النص المرئي: المهام
▶ مثال: تمرير معاملات إضافية باستخدام bind (مستوى الصعوبة: ⭐⭐)
المخرجات:
جلب البيانات من جانب الخادم يعرض قائمة بالعناصر.
المحتوى: المهام
يمكن استخدام Server Action بالاقتران مع خاصية action في <form> وطريقة .bind() لتمرير معاملات إضافية.
// app/bind-demo/page.tsx
import { revalidatePath } from 'next/cache'
export default async function BindDemoPage() {
const todos = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=5', {
next: { tags: ['bind-todos'] }
}).then(r => r.json())
return (
<div>
<h1>قائمة المهام مع Bind</h1>
<ul>{todos.map((t: any) => (
<li key={t.id}>
{t.title}
<form action={completeTodo.bind(null, t.id)} style={{ display: 'inline' }}>
<button type="submit">{t.completed ? '✅' : '⬜'}</button>
</form>
</li>
))}</ul>
</div>
)
}
async function completeTodo(id: number, formData: FormData) {
'use server'
await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`, {
method: 'PATCH', body: JSON.stringify({ completed: true })
})
revalidatePath('/bind-demo')
}
7. مثال كامل: نظام إدارة المهام
// app/tasks/page.tsx — مثال شامل لـ Server Actions
import { revalidatePath } from 'next/cache'
import { z } from 'zod'
// ======== المخطط ========
const taskSchema = z.object({
title: z.string().min(1, 'العنوان مطلوب').max(200),
priority: z.enum(['low', 'medium', 'high']),
assignee: z.string().min(1, 'المُسند إليه مطلوب'),
})
// ======== قائمة المهام ========
export default async function TasksPage() {
const tasks = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=10', {
next: { tags: ['tasks'] }
}).then(r => r.json())
return (
<div style={{ maxWidth: 800, margin: '0 auto', padding: 24 }}>
<h1>مدير المهام</h1>
<AddTaskForm />
<div style={{ marginTop: 24 }}>
{tasks.map((t: any) => (
<div key={t.id} style={{ border: '1px solid #ddd', borderRadius: 8, padding: 12, marginBottom: 8 }}>
<span>{t.title}</span>
<form action={deleteTask} style={{ display: 'inline', marginLeft: 12 }}>
<input type="hidden" name="id" value={t.id} />
<button type="submit" style={{ color: 'red' }}>حذف</button>
</form>
</div>
))}
</div>
</div>
)
}
// ======== نموذج مهمة جديدة ========
function AddTaskForm() {
return (
<form action={createTask} style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
<input name="title" required placeholder="عنوان المهمة" />
<select name="priority" defaultValue="medium">
<option value="low">منخفضة</option>
<option value="medium">متوسطة</option>
<option value="high">عالية</option>
</select>
<input name="assignee" required placeholder="المُسند إليه" />
<button type="submit">إضافة مهمة</button>
</form>
)
}
// ======== Server Actions ========
async function createTask(formData: FormData) {
'use server'
const validated = taskSchema.safeParse({
title: formData.get('title'),
priority: formData.get('priority'),
assignee: formData.get('assignee'),
})
if (!validated.success) {
console.error('Validation failed:', validated.error.flatten())
return
}
await fetch('https://jsonplaceholder.typicode.com/todos', {
method: 'POST', body: JSON.stringify({ title: validated.data.title, completed: false })
})
revalidatePath('/tasks')
}
async function deleteTask(formData: FormData) {
'use server'
const id = formData.get('id') as string
await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`, { method: 'DELETE' })
revalidatePath('/tasks')
}
❓ أسئلة شائعة
useActionState فقط في مكونات العميل؟useActionState هو hook من جانب العميل في React 19 يتطلب 'use client'. يدير حالات النموذج (قيد الانتظار، خطأ، نجاح) على جانب العميل. يمكن لمكونات الخادم الخالصة استخدام <form action={async} > في الوضع المضمن.'use server' المضمنة هي دالة وحدة مستقلة موسومة في وقت التجميع — لا يمكنها الوصول إلى المتغيرات داخل closure. إذا كنت بحاجة إلى استخدام props أو حالة المكون، يجب تمريرها عبر formData أو تعريفها في ملف actions.ts منفصل.formData أو تسلسل JSON.<form action={action}>، يتم تجاهل قيمة الإرجاع. لاسترداد قيمة الإرجاع، يجب استخدام useActionState(action, initialState) أو startTransition + callAction() صريح. نوصي باستخدام useActionState لإدارة قيم الإرجاع.📖 ملخص
- تُعرَّف Server Action باستخدام توجيه
'use server'وتدعم ثلاث طرق: ملفات مستقلة، ومضمنة، و useActionState <form action={action}>هي الطريقة الأكثر توصية لإرسال النماذج وتدعم التحسين التدريجي أصلاً- التحسين التدريجي يسمح بإرسال النماذج عبر POST الأصلي حتى عند تعطيل JavaScript
revalidatePath()وrevalidateTag()تُستخدمان في Server Action لتحديث البيانات المخزنة مؤقتاً بعد الإرسال- دمج Zod ينفذ تحققاً آمناً من حيث الأنواع لمعاملات Server Action
- تتضمن Server Actions حماية CSRF مضمنة؛ لا حاجة لإضافة رمز يدوياً
- استخدم
useActionState(action, initialState)لإدارة العناصر قيد الانتظار وقيم الإرجاع وحالات الخطأ
📝 تمارين
-
تمرين أساسي (⭐): أنشئ لوحة رسائل
app/guestbook/page.tsx. استخدم Server Action مضمنة لمعالجة إرسال النموذج، وأخرج محتوى الرسالة باستخدامconsole.log(لمحاكاة الكتابة إلى قاعدة البيانات)، وقم بتحديث الصفحةrevalidatePathبعد الإرسال. -
تمرين متقدم (⭐⭐): نفذ نظام إدارة مهام مع تحقق Zod في
app/todos-zod/page.tsx. متطلبات المخطط: العنوان (1-100 حرف)، الأولوية (تعداد: low/medium/high)، تاريخ الاستحقاق (اختياري، تنسيق YYYY-MM-DD). اعرض أخطاء التحقق على مستوى الحقل أسفل النموذج. -
تحدٍّ (⭐⭐⭐): أنشئ وحدة "إنشاء مشروع" كاملة:
app/projects/create/page.tsxتتضمن نموذجاً متعدد الحقول (الاسم، الوصف، تاريخ الاستحقاق، قائمة أعضاء الفريق)؛ عرِّف Server Actions لـ CRUD فيapp/actions/projects.ts؛ استخدمuseActionStateلإدارة حالة الإرسال (نجاح/خطأ/قيد الانتظار) على جانب العميل؛ وادعم إدخال رابط الصورة والمعاينة.