Next.js: Server Actions المتقدمة

آخر تحديث: 2026-08-26

عندما تلتقي Server Actions برفع الملفات والتحديثات المتفائلة — يحصل المستخدمون على "تغذية راجعة فورية" بدلاً من "أيقونات تحميل دوارة".

1. ما ستتعلمه



2. قصة حقيقية لمهندس واجهة أمامية

(1) نقطة الألم: يستغرق رفع الملفات 8 ثوانٍ، مما يدفع المستخدمين للاعتقاد بأن الصفحة قد تجمدت

تعمل ديانا على تطوير ميزة "رفع المرفقات" لتطبيق TaskFlow. بعد أن يختار المستخدم صورة بحجم 5 ميجابايت:

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

(2) حل Server Actions

استخدم useOptimistic لعرض مرفقات "افتراضية" فورًا في واجهة المستخدم + معالجة رفع موحدة عبر Server Action + معالجة الأخطاء بـ try-catch.

TSX
// app/tasks/[id]/Attachments.tsx — تحديث متفائل + رفع ملفات
'use client'
import { useOptimistic, useActionState } from 'react'
import { uploadAttachment } from './actions'

export function Attachments({ taskId, initialFiles }: Props) {
  const [optimisticFiles, addOptimistic] = useOptimistic(
    initialFiles,
    (state, newFile: File) => [...state, { id: 'pending', name: newFile.name, url: URL.createObjectURL(newFile), status: 'uploading' }]
  )

  const [error, formAction, pending] = useActionState(uploadAttachment, null)

  return (
    <form action={formAction}>
      <input type="hidden" name="taskId" value={taskId} />
      <input type="file" name="file" onChange={e => {
        const file = e.target.files?.[0]
        if (file) addOptimistic(file)  // عرض فوري في واجهة المستخدم
      }} />
      <button type="submit" disabled={pending}>رفع</button>
      {error && <p style={{ color: 'red' }}>{error}</p>}
      <ul>{optimisticFiles.map(f => <li key={f.id}>{f.name} {f.status === 'uploading' ? '⏳' : '✅'}</li>)}</ul>
    </form>
  )
}

(3) المكاسب

البُعد API Route التقليدية Server Action
زمن الاستجابة المدرك 6 ثوانٍ (بدون تغذية راجعة) فوري (تحديث متفائل)
عدد أسطر الكود 120 سطرًا (API + عميل + تحقق) 45 سطرًا
معالجة الأخطاء تتطلب try-catch يدويًا عبر السلسلة بأكملها مركزية عبر useActionState
التحقق من حجم الملف العميل والخادم بشكل منفصل موحد في Server Action


3. أوضاع معالجة الأخطاء الثلاثة

(1) نمط useActionState (موصى به)

TSX
// app/error-demo/use-action-state.tsx
'use client'
import { useActionState } from 'react'

async function submitOrder(prevState: any, formData: FormData) {
  'use server'
  try {
    const quantity = Number(formData.get('quantity'))
    if (quantity < 1) throw new Error('يجب أن تكون الكمية >= 1')
    if (quantity > 100) throw new Error('الكمية تتجاوز الحد المسموح')

    await db.order.create({ data: { quantity } })
    revalidateTag('orders')
    return { success: true, message: 'تم إنشاء الطلب' }
  } catch (err: any) {
    return { success: false, error: err.message }
  }
}

export default function OrderForm() {
  const [state, action, pending] = useActionState(submitOrder, null)

  return (
    <form action={action}>
      <input type="number" name="quantity" min={1} disabled={pending} />
      <button type="submit" disabled={pending}>{pending ? 'جاري الإرسال...' : 'إرسال'}</button>
      {state?.error && <p style={{ color: 'red' }}>{state.error}</p>}
      {state?.success && <p style={{ color: 'green' }}>{state.message}</p>}
    </form>
  )
}

(2) نمط startTransition

TSX
// app/error-demo/transition.tsx
'use client'
import { useTransition } from 'react'
import { submitFeedback } from './actions'

export function FeedbackForm() {
  const [isPending, startTransition] = useTransition()

  return (
    <form onSubmit={e => {
      e.preventDefault()
      const form = e.currentTarget
      const data = new FormData(form)
      startTransition(async () => {
        try {
          await submitFeedback(data)
          form.reset()
        } catch (err) {
          alert(`خطأ: ${err}`)
        }
      })
    }}>
      <textarea name="feedback" required disabled={isPending} />
      <button type="submit" disabled={isPending}>
        {isPending ? 'جاري الإرسال...' : 'إرسال الملاحظات'}
      </button>
    </form>
  )
}

(3) نمط try-catch (داخل Server Action)

TS
// app/actions/robust-action.ts
'use server'
import { revalidatePath } from 'next/cache'

export async function robustAction(formData: FormData) {
  const id = formData.get('id') as string

  // 1. التحقق من صحة المعاملات
  if (!id || isNaN(Number(id))) {
    throw new Error('معرف غير صالح')
  }

  // 2. منطق العمل + معالجة الأخطاء
  try {
    await db.item.update({ where: { id: Number(id) }, data: { status: 'processed' } })
    revalidatePath('/items')
    return { ok: true }
  } catch (dbError) {
    console.error('خطأ في قاعدة البيانات:', dbError)
    throw new Error('فشل تحديث العنصر في قاعدة البيانات')
  }
}

▶ مثال: مقارنة أنماط معالجة الأخطاء الثلاثة (المستوى: ⭐⭐)

الناتج:

TEXT 📖 للعرض فقط
تنفذ Server Action وتستدعي revalidatePath() لتحديث ذاكرة التخزين المؤقت للصفحة.
TSX
// app/error-comparison/page.tsx
import { revalidatePath } from 'next/cache'

// النمط 1: try-catch داخل Server Action + قيمة مرتجعة
async function createItem(formData: FormData) {
  'use server'
  try {
    const name = formData.get('name') as string
    if (!name || name.length < 2) return { error: 'الاسم قصير جدًا' }
    await fetch('https://jsonplaceholder.typicode.com/posts', { method: 'POST', body: JSON.stringify({ title: name }) })
    revalidatePath('/error-comparison')
    return { success: true }
  } catch (err) {
    return { error: 'خطأ في الشبكة' }
  }
}

export default function ErrorComparisonPage() {
  return (
    <div style={{ display: 'grid', gap: 32, padding: 24 }}>
      <section>
        <h2>النمط 1: حالة الإرجاع من Server Action</h2>
        <form action={createItem}>
          <input name="name" required />
          <button type="submit">إنشاء</button>
        </form>
      </section>
    </div>
  )
}

الناتج:

TEXT 📖 للعرض فقط
حقول النموذج: name. عند الإرسال، تعالج البيانات وتُحدّث ذاكرة التخزين المؤقت للصفحة.
المحتوى المرئي: النمط 1: حالة الإرجاع من Server Action | إنشاء


4. useOptimistic: التحديثات المتفائلة

useOptimistic هو hook جديد في React 19 يتيح لك تحديث واجهة المستخدم فورًا قبل اكتمال server action، ثم التراجع تلقائيًا أو تأكيد التحديث عند عودة النتيجة الفعلية.

100%
sequenceDiagram
    participant User as المستخدم
    participant UI as واجهة العميل
    participant SA as Server Action
    participant DB as قاعدة البيانات

    User->>UI: النقر على "إكمال المهمة"
    UI->>UI: useOptimistic → تحول إلى الرمادي فورًا + علامة صح
    UI->>SA: استدعاء Server Action
    SA->>DB: تحديث قاعدة البيانات
    DB-->>SA: نجاح
    SA-->>UI: إرجاع النتائج
    UI->>UI: مقارنة مع الوضع الفعلي → تثبيت أو تراجع
الحالة عرض واجهة المستخدم البيانات الفعلية إدراك المستخدم
تحديث الحالة ✅ مكتمل (علامة صح) ⏳ قيد التنفيذ فوري
تأكيد الخادم ✅ مكتمل ✅ مكتمل لا تغيير
رفض الخادم ❌ غير مكتمل (تراجع) ❌ غير مكتمل حركة تراجع

▶ مثال: تحديث متفائل لحالة المهمة (المستوى: ⭐⭐⭐)

الناتج:

TEXT 📖 للعرض فقط
مخطط تسلسلي يوضح تدفق الرسائل بين المشاركين.
TSX
// app/todos/optimistic-list.tsx
'use client'
import { useOptimistic } from 'react'
import { revalidatePath } from 'next/cache'

type Todo = { id: number; title: string; completed: boolean }

export function OptimisticTodoList({ todos: initialTodos }: { todos: Todo[] }) {
  const [todos, addOptimistic] = useOptimistic(
    initialTodos,
    (state, updatedTodo: Todo) =>
      state.map(t => t.id === updatedTodo.id ? { ...t, completed: !t.completed } : t)
  )

  return (
    <ul>{todos.map(todo => (
      <li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
        {todo.title}
        <form action={async (formData: FormData) => {
          'use server'
          const id = Number(formData.get('id'))
          await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`, {
            method: 'PATCH', body: JSON.stringify({ completed: true })
          })
          revalidatePath('/todos/optimistic')
        }} onSubmit={e => {
          // تحديث متفائل: تحديث فوري قبل إرسال النموذج إلى واجهة المستخدم
          addOptimistic({ ...todo, completed: !todo.completed })
        }}>
          <input type="hidden" name="id" value={todo.id} />
          <button type="submit">{todo.completed ? 'تراجع' : 'إكمال'}</button>
        </form>
      </li>
    ))}</ul>
  )
}

الناتج:

TEXT 📖 للعرض فقط
عند الإرسال، تعالج البيانات وتُحدّث ذاكرة التخزين المؤقت للصفحة.


5. معالجة رفع الملفات

تستطيع Server Actions استقبال الملفات مباشرة عبر formData. يعالج الخادم تدفق الملف ويدعم التحقق من الحجم والتحقق من النوع ومعالجة التخزين.

(1) معالجة الملفات من جهة الخادم

TS
// app/actions/upload.ts
'use server'
import { revalidateTag } from 'next/cache'
import { writeFile } from 'node:fs/promises'
import { join } from 'node:path'

const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf']
const MAX_SIZE = 5 * 1024 * 1024  // 5 ميجابايت

export async function uploadAvatar(prevState: any, formData: FormData) {
  const file = formData.get('avatar') as File | null
  if (!file) return { error: 'لم يتم اختيار ملف' }

  // التحقق من النوع
  if (!ALLOWED_TYPES.includes(file.type)) {
    return { error: 'نوع ملف غير صالح. المسموح: JPEG, PNG, WebP, PDF' }
  }

  // التحقق من الحجم
  if (file.size > MAX_SIZE) {
    return { error: `الملف كبير جدًا. الحد الأقصى: ${MAX_SIZE / 1024 / 1024} ميجابايت` }
  }

  try {
    const bytes = await file.arrayBuffer()
    const buffer = Buffer.from(bytes)
    const filename = `${Date.now()}-${file.name.replace(/\s/g, '_')}`
    const path = join('public/uploads', filename)
    await writeFile(path, buffer)

    // حفظ في قاعدة البيانات
    await db.avatar.create({ data: { filename, path: `/uploads/${filename}` } })
    revalidateTag('avatars')
    return { success: true, url: `/uploads/${filename}` }
  } catch (err) {
    return { error: 'فشل رفع الملف' }
  }
}

(2) إرسال النموذج من جهة العميل

TSX
// app/profile/avatar-upload.tsx
'use client'
import { useActionState } from 'react'
import { uploadAvatar } from '@/app/actions/upload'

export function AvatarUpload() {
  const [state, action, pending] = useActionState(uploadAvatar, null)

  return (
    <form action={action}>
      <input type="file" name="avatar" accept="image/jpeg,image/png,image/webp" disabled={pending} />
      <button type="submit" disabled={pending}>
        {pending ? 'جاري الرفع...' : 'رفع الصورة الرمزية'}
      </button>
      {state?.error && <p style={{ color: 'red' }}>{state.error}</p>}
      {state?.success && state?.url && (
        <div>
          <p style={{ color: 'green' }}>تم الرفع بنجاح!</p>
          <img src={state.url} alt="صورة رمزية" style={{ width: 100, height: 100, borderRadius: '50%' }} />
        </div>
      )}
    </form>
  )
}

(3) رفع ملفات متعددة

TSX
// app/actions/multi-upload.ts
'use server'
import { revalidateTag } from 'next/cache'

export async function uploadGallery(prevState: any, formData: FormData) {
  const files = formData.getAll('photos') as File[]
  if (files.length === 0) return { error: 'لم يتم اختيار ملفات' }
  if (files.length > 10) return { error: 'الحد الأقصى 10 ملفات' }

  const uploaded: string[] = []
  for (const file of files) {
    if (file.size > 5 * 1024 * 1024) continue
    const bytes = await file.arrayBuffer()
    const buffer = Buffer.from(bytes)
    const filename = `${Date.now()}-${file.name}`
    await writeFile(join('public/uploads', filename), buffer)
    uploaded.push(`/uploads/${filename}`)
  }

  revalidateTag('gallery')
  return { success: true, urls: uploaded }
}

▶ مثال: رفع ملفات كامل + معاينة (المستوى: ⭐⭐⭐)

الناتج:

TEXT 📖 للعرض فقط
يعرض واجهة مستخدم مكون uploadGallery.
TSX
// app/upload-demo/page.tsx — نظرة عامة على رفع الملفات
import { revalidatePath } from 'next/cache'
import { writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import fs from 'node:fs'

export default async function UploadDemoPage() {
  // قراءة قائمة الملفات الموجودة
  const uploadDir = join(process.cwd(), 'public', 'uploads')
  const files = fs.existsSync(uploadDir) ? fs.readdirSync(uploadDir) : []

  return (
    <div style={{ maxWidth: 600, margin: '0 auto', padding: 24 }}>
      <h1>عرض رفع الملفات</h1>

      <form action={async (formData: FormData) => {
        'use server'
        const file = formData.get('file') as File
        if (!file) return

        if (file.size > 2 * 1024 * 1024) {
          return { error: 'الملف كبير جدًا (الحد الأقصى 2 ميجابايت)' }
        }

        const bytes = await file.arrayBuffer()
        const buffer = Buffer.from(bytes)
        const filename = `${Date.now()}-${file.name}`
        await writeFile(join('public/uploads', filename), buffer)
        revalidatePath('/upload-demo')
      }}>
        <input type="file" name="file" required />
        <button type="submit">رفع</button>
      </form>

      <div style={{ marginTop: 24 }}>
        <h2>الملفات المرفوعة ({files.length})</h2>
        <ul>{files.map(f => (
          <li key={f}>
            <a href={`/uploads/${f}`} target="_blank">{f}</a>
          </li>
        ))}</ul>
      </div>
    </div>
  )
}

الناتج:

TEXT 📖 للعرض فقط
عند الإرسال، تعالج البيانات وتُحدّث ذاكرة التخزين المؤقت للصفحة.
المحتوى المرئي: عرض رفع الملفات


6. الكتابة في قاعدة البيانات + إبطال صلاحية التخزين المؤقت + معاملة إعادة التوجيه

تتكون server actions الإنتاجية عادةً من ثلاث خطوات: الكتابة في قاعدة البيانات → مسح ذاكرة التخزين المؤقت → إعادة توجيه المستخدم.

100%
sequenceDiagram
    participant User as المستخدم
    participant Action as Server Action
    participant DB as قاعدة البيانات
    participant Cache as طبقة التخزين المؤقت

    User->>Action: إرسال النموذج
    Action->>Action: التحقق عبر Zod
    Action->>DB: INSERT / UPDATE
    DB-->>Action: نجاح
    Action->>Cache: revalidateTag / revalidatePath
    Action->>User: redirect الانتقال إلى صفحة جديدة/صفحة التفاصيل
الخطوة API الوصف
كتابة db.create() / db.update() عمليات قاعدة البيانات عبر Prisma / Drizzle
إبطال صلاحية التخزين المؤقت revalidateTag() / revalidatePath() ضمان عرض أحدث البيانات في صفحة القائمة
إعادة توجيه redirect() إعادة التوجيه إلى صفحة التفاصيل أو صفحة القائمة بعد الإرسال

▶ مثال: نمط المعاملة الكامل (المستوى: ⭐⭐⭐)

الناتج:

TEXT 📖 للعرض فقط
مخطط تسلسلي يوضح تدفق الرسائل بين المشاركين.
TSX
// app/posts/create/page.tsx — كتابة + إبطال صلاحية التخزين المؤقت + إعادة توجيه
import { revalidateTag } from 'next/cache'
import { redirect } from 'next/navigation'
import { z } from 'zod'

const postSchema = z.object({
  title: z.string().min(5).max(200),
  content: z.string().min(20),
  published: z.coerce.boolean().default(false),
})

export default function CreatePostPage() {
  return (
    <form action={async (formData: FormData) => {
      'use server'
      // 1. التحقق
      const validated = postSchema.safeParse({
        title: formData.get('title'),
        content: formData.get('content'),
        published: formData.get('published'),
      })
      if (!validated.success) return { errors: validated.error.flatten().fieldErrors }

      // 2. الكتابة في قاعدة البيانات
      const post = await fetch('https://jsonplaceholder.typicode.com/posts', {
        method: 'POST',
        body: JSON.stringify({
          title: validated.data.title,
          body: validated.data.content,
          userId: 1,
        })
      }).then(r => r.json())

      // 3. إبطال صلاحية التخزين المؤقت
      revalidateTag('posts')
      revalidatePath('/posts')

      // 4. إعادة التوجيه إلى المقال الجديد
      redirect(`/posts/${post.id}`)
    }}>
      <div><input name="title" required placeholder="عنوان المقال" /></div>
      <div><textarea name="content" required placeholder="المحتوى" rows={10} /></div>
      <div><label><input name="published" type="checkbox" value="true" /> منشور</label></div>
      <button type="submit">إنشاء مقال</button>
    </form>
  )
}


### ▶ مثال: startTransition + معالجة الأخطاء من جهة العميل (المستوى: ⭐⭐⭐)

الناتج:

TEXT 📖 للعرض فقط
عند الإرسال، تعالج البيانات من جهة الخادم وتُعيد التوجيه.

'use client' import { useTransition, useState } from 'react' import { revalidateTag } from 'next/cache'

async function subscribeNewsletter(formData: FormData) { 'use server' const email = formData.get('email') as string if (!email || !email.includes('@')) throw new Error('بريد إلكتروني غير صالح') await fetch('https://jsonplaceholder.typicode.com/posts', { method: 'POST', body: JSON.stringify({ title: email, body: 'اشتراك في النشرة البريدية' }) }) revalidateTag('newsletter') }

export default function NewsletterForm() { const [isPending, startTransition] = useTransition() const [error, setError] = useState<string | null>(null)

return ( <form onSubmit={e => { e.preventDefault() const form = e.currentTarget const data = new FormData(form) setError(null) startTransition(async () => { try { await subscribeNewsletter(data) form.reset() } catch (err: any) { setError(err.message) } }) }}> <input type="email" name="email" required placeholder="بريدك@example.com" disabled={isPending} /> <button type="submit" disabled={isPending}> {isPending ? 'جاري الاشتراك...' : 'اشتراك'} </button> {error && <p style={{ color: 'red' }}>خطأ: {error}</p>} </form> ) }

TEXT 📖 للعرض فقط

---

---

## 7. Server Actions مقابل API Routes: معايير الاختيار

| البُعد | Server Actions | API Routes |
|:-----|:--------------|:-----------|
| طريقة الاستدعاء | بروتوكول RSC Payload | HTTP (REST) |
| السيناريوهات المناسبة | نماذج الطرف الأول، إجراءات المستخدم | API الطرف الثالث، Webhook، تطبيقات الجوال |
| حماية CSRF | ✅ مدمجة | ❌ يجب تنفيذها يدويًا |
| التحسين التدريجي | ✅ مدعوم | ❌ يتطلب JavaScript |
| أمان الأنواع | ✅ أنواع TypeScript كاملة | ⚠️ معالجة يدوية |
| رفع الملفات | ✅ معالجة مباشرة عبر formData | ✅ معالجة تدفق req |
| طرق المصادقة | دالة `auth()` | Middleware / JWT |
| تحديد المعدل | ⚠️ يجب تنفيذه يدويًا | ✅ معالجة مركزية عبر Middleware |
| إبطال صلاحية التخزين المؤقت | ✅ revalidate مدمج | ✅ يمكن استدعاء revalidateTag |
| صعوبة التصحيح | منخفضة (استدعاء دالة) | متوسطة (تصحيح HTTP) |

### (8) ▶ توصيات الاختيار

الناتج:

TEXT 📖 للعرض فقط
مكتمل.

graph TB A[ما نوع الواجهة المطلوبة؟] --> B{من هو المستدعي؟} B -->|استدعاء مباشر من المتصفح| C{هل يوجد نموذج؟} B -->|طرف ثالث / Webhook / تطبيق جوال| D[API Route] C -->|نموذج| E[Server Action ✅] C -->|لا، تفاعل JSON| F{هل تحتاج إلى رفع ملف؟} F -->|نعم| E F -->|لا| D

TEXT 📖 للعرض فقط

---

---

## 8. مثال كامل: تفاصيل مهمة مع تحديثات متفائلة + رفع ملفات

// app/tasks/[id]/page.tsx — Server Actions متقدمة شاملة import { revalidateTag } from 'next/cache' import { writeFile } from 'node:fs/promises' import { join } from 'node:path' import { z } from 'zod'

// ======== المخططات ======== const commentSchema = z.object({ content: z.string().min(1).max(1000), author: z.string().min(2).max(100), })

// ======== صفحة تفاصيل المهمة ======== export default async function TaskDetailPage({ params }: { params: { id: string } }) { const task = await fetch(https://jsonplaceholder.typicode.com/todos/${params.id}).then(r => r.json()) const comments = await fetch(https://jsonplaceholder.typicode.com/posts/${params.id}/comments, { next: { tags: [comments-${params.id}] } }).then(r => r.json())

return ( <div style={{ maxWidth: 800, margin: '0 auto', padding: 24 }}> <h1>{task.title}</h1> <p>الحالة: {task.completed ? '✅ مكتمل' : '⏳ قيد الانتظار'}</p>

  {/* تبديل سريع للحالة */}
  `<form action={async (formData: FormData) =>` {
    'use server'
    const id = Number(formData.get('id'))
    const completed = formData.get('completed') === 'true'
    await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`, {
      method: 'PATCH',
      body: JSON.stringify({ completed: !completed })
    })
    revalidateTag(`task-${id}`)
  }}>
    `<input type="hidden" name="id" value={params.id} />`
    `<input type="hidden" name="completed" value={String(task.completed)} />`
    `<button type="submit">`{task.completed ? 'تعليم كقيد الانتظار' : 'تعليم كمكتمل'}`</button>`
  `</form>`

  {/* إضافة تعليق */}
  `<h2>`التعليقات`</h2>`
  `<form action={async (formData: FormData) =>` {
    'use server'
    const validated = commentSchema.safeParse({
      content: formData.get('content'),
      author: formData.get('author'),
    })
    if (!validated.success) return { errors: validated.error.flatten().fieldErrors }
    await fetch('https://jsonplaceholder.typicode.com/comments', {
      method: 'POST',
      body: JSON.stringify({
        postId: Number(params.id),
        name: validated.data.author,
        body: validated.data.content,
        email: 'user@example.com',
      })
    })
    revalidateTag(`comments-${params.id}`)
  }}>
    `<div>`<input name="author" required placeholder="اسمك" />`</div>
    `<div>`<textarea name="content" required placeholder="تعليق" />`</div>
    `<button type="submit">`إضافة تعليق`</button>`
  `</form>`

  `<ul>`{comments.map((c: any) => (
    `<li key={c.id}>`<strong>`{c.name}:`</strong>` {c.body}`</li>`
  I don't know what this is.

  {/* رفع ملفات */}
  `<h2>`المرفقات`</h2>`
  `<form action={async (formData: FormData) =>` {
    'use server'
    const file = formData.get('file') as File
    if (!file || file.size === 0) return { error: 'لا يوجد ملف' }
    if (file.size > 5 * 1024 * 1024) return { error: 'الملف كبير جدًا' }
    const bytes = await file.arrayBuffer()
    await writeFile(join('public/uploads', `${Date.now()}-${file.name}`), Buffer.from(bytes))
    revalidateTag(`attachments-${params.id}`)
  }}>
    `<input type="file" name="file" />`
    `<button type="submit">`رفع`</button>`
  `</form>`
`</div>`

) }


❓ أسئلة شائعة

س هل يمكن استخدام useOptimistic و useActionState معًا؟
ج نعم. يُستخدم useOptimistic لتحديث واجهة المستخدم مسبقًا، بينما يُستخدم useActionState لاسترداد الحالة النهائية التي يُرجعها الخادم فعليًا. النمط الشائع هو استخدام useOptimistic لإدارة تغييرات واجهة المستخدم الفورية و useActionState للتأكيد أو التراجع بعد استلام استجابة الخادم.
س ما أنواع الملفات المدعومة لرفع الملفات في Server Action؟
ج أي نوع ملف يدعمه المتصفح مقبول. يمكن للخادم التحقق من الملف عبر قراءة file.type (نوع MIME) و file.size (عدد البايتات). النهج الموصى به هو حفظ الملف في نظام الملفات أو التخزين السحابي (S3/R2) وتخزين مرجع مسار له في قاعدة البيانات.
س كيف يُستخدم redirect() في Server Action؟
ج يجب استيراد redirect() من next/navigation. عند استدعائه في Server Action، يُلقي استثناء إعادة توجيه خاص. يجب استدعاؤه خارج كتلة try-catch — إذا وُضع داخل كتلة try، فسيتم التقاطه بواسطة كتلة catch. النمط الصحيح هو: التحقق → الكتابة → revalidate → redirect (ليس داخل كتلة try).
س ما هو وقت انتهاء المهلة لـ Server Actions؟
ج خطة Vercel المجانية: 10 ثوانٍ (Serverless Function)، 60 ثانية (Pro). الاستضافة الذاتية: يحددها إعداد خادم Node.js الخاص بك (غير محدود افتراضيًا). للعمليات طويلة الأمد (مثل معالجة الفيديو)، نوصي بتقسيمها إلى مهام غير متزامنة مع استدعاءات Webhook، بدلاً من الانتظار المتزامن داخل Server Action.
س هل يمكن لـ Server Action استدعاء دالة set-cookie عدة مرات؟
ج نعم. استخدم دالة cookies() لتعيين رأس الاستجابة: const cookieStore = cookies(); cookieStore.set('theme', 'dark'). لكن لاحظ أن عمليات الكوكيز في Server Action تسري بشكل مجمع؛ لا يمكنك تعيين كوكي ثم قراءته ضمن نفس الإجراء.
س في أي سيناريوهات يجب استخدام Server Action بدلاً من API Route؟
ج هناك ثلاثة سيناريوهات تُفضل فيها Server Actions: ① إجراءات المستخدم من الطرف الأول (إرسال النماذج، نقرات الأزرار)؛ ② المواقف التي تتطلب تحسينًا تدريجيًا (لا تزال قابلة للاستخدام حتى عند تعطيل JavaScript)؛ ③ التكامل الوثيق مع نظام التخزين المؤقت لـ RSC (revalidateTag/revalidatePath). مزايا API Routes هي: ① تكاملات الطرف الثالث (تطبيقات الجوال، webhooks)؛ ② عدم الحاجة إلى سياق صفحة HTML؛ ③ متطلب APIs RESTful قياسية.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (⭐): أنشئ app/quick-todo/page.tsx واستخدم Server Action مضمنة لإضافة وحذف المهام. في Server Action، استخدم كتلة try-catch لمعالجة الأخطاء وإرجاع { error: string } إلى العميل للعرض.

  2. تمرين متقدم (⭐⭐): أنشئ صفحة app/gallery/page.tsx لرفع صور متعددة تدعم تحديد حتى 5 صور في المرة الواحدة. في Server Action، تحقق من نوع الملف (صور فقط) والحجم (≤ 2 ميجابايت لكل صورة)، واعرض قائمة بالصور المصغرة بعد الرفع. نفذ تخزين writeFile وتحديث revalidatePath في Server Action.

  3. تحدي (⭐⭐⭐): نفذ صفحة "لوحة مهام" تتضمن ثلاثة أعمدة للحالة (قيد الانتظار / قيد التنفيذ / مكتمل). استخدم useOptimistic لتنفيذ تحديثات فورية لواجهة المستخدم عند تبديل الحالات عبر السحب والإفلات (لا حاجة للسحب الفعلي؛ استخدم الأزرار للتبديل بدلاً من ذلك). بعد كتابة Server Action، قم بتحديث revalidateTag تلقائيًا لجميع الأعمدة. أضف ميزة رفع الملفات إلى كل بطاقة مهمة.

Web-Tutorial.com

فريق Web-Tutorial التقني

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

100%