Next.js: Server Actions المتقدمة
آخر تحديث: 2026-08-26
عندما تلتقي Server Actions برفع الملفات والتحديثات المتفائلة — يحصل المستخدمون على "تغذية راجعة فورية" بدلاً من "أيقونات تحميل دوارة".
1. ما ستتعلمه
useActionState/startTransition/try-catch— أوضاع معالجة الأخطاء الثلاثة- تنفيذ Hook
useOptimisticللتحديثات المتفائلة - رفع الملفات عبر
formDataوالمعالجة من جهة الخادم - نمط المعاملة الذي يشمل: الكتابة في قاعدة البيانات + إبطال صلاحية التخزين المؤقت + إعادة التوجيه
- مقارنة اختيارية بين Server Actions و API Routes
2. قصة حقيقية لمهندس واجهة أمامية
(1) نقطة الألم: يستغرق رفع الملفات 8 ثوانٍ، مما يدفع المستخدمين للاعتقاد بأن الصفحة قد تجمدت
تعمل ديانا على تطوير ميزة "رفع المرفقات" لتطبيق TaskFlow. بعد أن يختار المستخدم صورة بحجم 5 ميجابايت:
- ترسل الواجهة الأمامية
fetchإلى/api/upload(رفع لمدة ثانيتين) - يستخدم الخادم
sharpلمعالجة الصور المصغرة (3 ثوانٍ) - استدعاء
revalidateTagلتحديث قائمة المرفقات (0.5 ثانية) - إعادة التوجيه إلى صفحة التفاصيل (0.5 ثانية)
- الإجمالي: حوالي 6 ثوانٍ
والأسوأ من ذلك، لا توجد أي تغذية راجعة في واجهة المستخدم خلال تلك الثواني الست — يعتقد المستخدمون أن الصفحة قد تجمدت ويستمرون في النقر على زر الإرسال.
(2) حل Server Actions
استخدم
useOptimisticلعرض مرفقات "افتراضية" فورًا في واجهة المستخدم + معالجة رفع موحدة عبر Server Action + معالجة الأخطاء بـ try-catch.
// 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 (موصى به)
// 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
// 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)
// 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('فشل تحديث العنصر في قاعدة البيانات')
}
}
▶ مثال: مقارنة أنماط معالجة الأخطاء الثلاثة (المستوى: ⭐⭐)
الناتج:
تنفذ Server Action وتستدعي revalidatePath() لتحديث ذاكرة التخزين المؤقت للصفحة.
// 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>
)
}
الناتج:
حقول النموذج: name. عند الإرسال، تعالج البيانات وتُحدّث ذاكرة التخزين المؤقت للصفحة.
المحتوى المرئي: النمط 1: حالة الإرجاع من Server Action | إنشاء
4. useOptimistic: التحديثات المتفائلة
useOptimistic هو hook جديد في React 19 يتيح لك تحديث واجهة المستخدم فورًا قبل اكتمال server action، ثم التراجع تلقائيًا أو تأكيد التحديث عند عودة النتيجة الفعلية.
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: مقارنة مع الوضع الفعلي → تثبيت أو تراجع
| الحالة | عرض واجهة المستخدم | البيانات الفعلية | إدراك المستخدم |
|---|---|---|---|
| تحديث الحالة | ✅ مكتمل (علامة صح) | ⏳ قيد التنفيذ | فوري |
| تأكيد الخادم | ✅ مكتمل | ✅ مكتمل | لا تغيير |
| رفض الخادم | ❌ غير مكتمل (تراجع) | ❌ غير مكتمل | حركة تراجع |
▶ مثال: تحديث متفائل لحالة المهمة (المستوى: ⭐⭐⭐)
الناتج:
مخطط تسلسلي يوضح تدفق الرسائل بين المشاركين.
// 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>
)
}
الناتج:
عند الإرسال، تعالج البيانات وتُحدّث ذاكرة التخزين المؤقت للصفحة.
5. معالجة رفع الملفات
تستطيع Server Actions استقبال الملفات مباشرة عبر formData. يعالج الخادم تدفق الملف ويدعم التحقق من الحجم والتحقق من النوع ومعالجة التخزين.
(1) معالجة الملفات من جهة الخادم
// 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) إرسال النموذج من جهة العميل
// 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) رفع ملفات متعددة
// 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 }
}
▶ مثال: رفع ملفات كامل + معاينة (المستوى: ⭐⭐⭐)
الناتج:
يعرض واجهة مستخدم مكون uploadGallery.
// 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>
)
}
الناتج:
عند الإرسال، تعالج البيانات وتُحدّث ذاكرة التخزين المؤقت للصفحة.
المحتوى المرئي: عرض رفع الملفات
6. الكتابة في قاعدة البيانات + إبطال صلاحية التخزين المؤقت + معاملة إعادة التوجيه
تتكون server actions الإنتاجية عادةً من ثلاث خطوات: الكتابة في قاعدة البيانات → مسح ذاكرة التخزين المؤقت → إعادة توجيه المستخدم.
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() |
إعادة التوجيه إلى صفحة التفاصيل أو صفحة القائمة بعد الإرسال |
▶ مثال: نمط المعاملة الكامل (المستوى: ⭐⭐⭐)
الناتج:
مخطط تسلسلي يوضح تدفق الرسائل بين المشاركين.
// 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 + معالجة الأخطاء من جهة العميل (المستوى: ⭐⭐⭐)
الناتج:
عند الإرسال، تعالج البيانات من جهة الخادم وتُعيد التوجيه.
'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>
)
}
---
---
## 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) ▶ توصيات الاختيار
الناتج:
مكتمل.
graph TB A[ما نوع الواجهة المطلوبة؟] --> B{من هو المستدعي؟} B -->|استدعاء مباشر من المتصفح| C{هل يوجد نموذج؟} B -->|طرف ثالث / Webhook / تطبيق جوال| D[API Route] C -->|نموذج| E[Server Action ✅] C -->|لا، تفاعل JSON| F{هل تحتاج إلى رفع ملف؟} F -->|نعم| E F -->|لا| D
---
---
## 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 للتأكيد أو التراجع بعد استلام استجابة الخادم.file.type (نوع MIME) و file.size (عدد البايتات). النهج الموصى به هو حفظ الملف في نظام الملفات أو التخزين السحابي (S3/R2) وتخزين مرجع مسار له في قاعدة البيانات.redirect() في Server Action؟redirect() من next/navigation. عند استدعائه في Server Action، يُلقي استثناء إعادة توجيه خاص. يجب استدعاؤه خارج كتلة try-catch — إذا وُضع داخل كتلة try، فسيتم التقاطه بواسطة كتلة catch. النمط الصحيح هو: التحقق → الكتابة → revalidate → redirect (ليس داخل كتلة try).set-cookie عدة مرات؟cookies() لتعيين رأس الاستجابة: const cookieStore = cookies(); cookieStore.set('theme', 'dark'). لكن لاحظ أن عمليات الكوكيز في Server Action تسري بشكل مجمع؛ لا يمكنك تعيين كوكي ثم قراءته ضمن نفس الإجراء.📖 ملخص
- ثلاثة أوضاع لمعالجة الأخطاء: useActionState (موصى به)، startTransition، try-catch
useOptimisticيُحدث واجهة المستخدم في الوقت الفعلي قبل اكتمال Server Action، ويتراجع تلقائيًا في حالة الفشل- لرفع الملفات، استخدم
formData.get('file') as Fileلاسترداد تدفق الملف والتحقق من نوع الملف وحجمه - تدفق المعاملة: التحقق عبر Zod → الكتابة في قاعدة البيانات → revalidateTag → redirect
- Server Actions مناسبة لعمليات نماذج الطرف الأول، بينما API Routes مناسبة لتكاملات الطرف الثالث
- لرفع الملفات الكبيرة، يُوصى بتقسيمها إلى مهام غير متزامنة للمعالجة
- لا يمكن استدعاء
redirect()داخل كتلةtry؛ يجب وضعه في نهاية المعاملة
📝 تمارين
-
تمرين أساسي (⭐): أنشئ
app/quick-todo/page.tsxواستخدم Server Action مضمنة لإضافة وحذف المهام. في Server Action، استخدم كتلة try-catch لمعالجة الأخطاء وإرجاع{ error: string }إلى العميل للعرض. -
تمرين متقدم (⭐⭐): أنشئ صفحة
app/gallery/page.tsxلرفع صور متعددة تدعم تحديد حتى 5 صور في المرة الواحدة. في Server Action، تحقق من نوع الملف (صور فقط) والحجم (≤ 2 ميجابايت لكل صورة)، واعرض قائمة بالصور المصغرة بعد الرفع. نفذ تخزينwriteFileوتحديثrevalidatePathفي Server Action. -
تحدي (⭐⭐⭐): نفذ صفحة "لوحة مهام" تتضمن ثلاثة أعمدة للحالة (قيد الانتظار / قيد التنفيذ / مكتمل). استخدم
useOptimisticلتنفيذ تحديثات فورية لواجهة المستخدم عند تبديل الحالات عبر السحب والإفلات (لا حاجة للسحب الفعلي؛ استخدم الأزرار للتبديل بدلاً من ذلك). بعد كتابة Server Action، قم بتحديثrevalidateTagتلقائيًا لجميع الأعمدة. أضف ميزة رفع الملفات إلى كل بطاقة مهمة.