Next.js: أساسيات Server Actions

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

Server Actions هي "القوة الخارقة" لـ Next.js — إنها تسمح للمتصفح باستدعاء دوال جانب الخادم مباشرة، دون الحاجة إلى بناء نقاط نهاية API يدوياً.

1. ما ستتعلمه



2. قصة حقيقية لمطور Full-Stack

(1) نقطة الألم: يستغرق 80 سطراً من الكود لإرسال نموذج "بسيط"

يقوم بوب بإضافة ميزة "إنشاء مشروع" إلى TaskFlow. يتطلب النهج التقليدي:

تنهد بوب قائلاً: "أنا فقط أريد إرسال نموذج."

(2) حل Server Actions

معالجة النماذج مباشرة باستخدام دالة 'use server' واحدة — بدون عبء JavaScript من جانب العميل، بدون مسارات API.

TSX
// 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

100%
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) وضع الملف المنفصل (موصى به)

TS
// 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')
}
TSX
// 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) الوضع المضمن (السيناريوهات البسيطة)

TSX
// 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 (موصى به للنماذج ذات الحالة)

TSX
// 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>
  )
}

▶ مثال: مقارنة طرق التعريف الثلاث (مستوى الصعوبة: ⭐)

المخرجات:

TEXT 📖 للعرض فقط
نموذج يحتوي على حقول: todo.
عند الإرسال، يقوم Server Action بمعالجة البيانات وإبطال وسوم التخزين المؤقت ذات الصلة.
TSX
// 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>
  )
}

المخرجات:

TEXT 📖 للعرض فقط
حقول النموذج: title. عند الإرسال، يعالج بيانات النموذج على الخادم.
المحتوى المرئي: Server Actions — 3 طرق | 1. ملف منفصل | Create
TSX
// 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>
  )
}
TS
// 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 الأصلي

TSX
// 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 (مستوى الصعوبة ⭐⭐)

المخرجات:

TEXT 📖 للعرض فقط
نموذج يحتوي على حقول إدخال وزر إرسال.
عند الإرسال، يقوم Server Action بمعالجة البيانات وتحديث التخزين المؤقت للصفحة.
TSX
// 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>
  )
}

المخرجات:

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

طريقة الاختبار:

  1. الاستخدام العادي: أدخل نصاً → انقر "إرسال" → يتم تحديث القائمة
  2. تعطيل JavaScript: DevTools → الإعدادات → تعطيل JavaScript → تحديث → إرسال النموذج → لا يزال يعمل


6. دمج التحقق باستخدام Zod

يجب على Server Actions دائماً التحقق من صحة بيانات الإدخال. Zod هي مكتبة التحقق الأكثر شيوعاً في نظام TypeScript البيئي.

(1) وضع التحقق الأساسي

TS
// 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(),
})
TS
// 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) عرض خطأ التحقق في العميل

TSX
// 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 كامل (مستوى الصعوبة: ⭐⭐)

المخرجات:

TEXT 📖 للعرض فقط
نموذج مع تحديثات واجهة مستخدم متفائلة باستخدام useActionState.
النص المرئي: } | } | تم إنشاء المشروع! | }
      {state?.error &&
TSX
// 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>
  )
}

المخرجات:

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

▶ مثال: revalidatePath لتحديث القائمة (مستوى الصعوبة: ⭐)

المخرجات:

TEXT 📖 للعرض فقط
يعرض المكون واجهة المستخدم الموصوفة في المتصفح.
TSX
// 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>
  )
}

المخرجات:

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

▶ مثال: تمرير معاملات إضافية باستخدام bind (مستوى الصعوبة: ⭐⭐)

المخرجات:

TEXT 📖 للعرض فقط
جلب البيانات من جانب الخادم يعرض قائمة بالعناصر.
المحتوى: المهام

يمكن استخدام Server Action بالاقتران مع خاصية action في <form> وطريقة .bind() لتمرير معاملات إضافية.

TSX
// 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. مثال كامل: نظام إدارة المهام

TSX
// 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')
}

❓ أسئلة شائعة

س ما الفرق بين Server Action ومسار API التقليدي؟
ج Server Action هي دالة من جانب الخادم يستدعيها المتصفح مباشرة (عبر بروتوكول RSC Payload)؛ لا تتطلب نقطة نهاية HTTP، وتعالج CSRF تلقائياً، وتدعم التحسين التدريجي. مسارات API هي نقاط نهاية HTTP، مناسبة لتكاملات الطرف الثالث و webhooks والعملاء غير المتصفحين. الاثنان غير قابلين للتبادل — Server Actions تتعامل مع نماذج الطرف الأول، بينما مسارات API تتعامل مع APIs الطرف الثالث.
س هل يمكن استخدام useActionState فقط في مكونات العميل؟
ج نعم. useActionState هو hook من جانب العميل في React 19 يتطلب 'use client'. يدير حالات النموذج (قيد الانتظار، خطأ، نجاح) على جانب العميل. يمكن لمكونات الخادم الخالصة استخدام <form action={async} > في الوضع المضمن.
س كيف تمنع Server Actions هجمات CSRF؟
ج يتضمن Next.js 16 حماية CSRF مضمنة. لا يمكن استدعاء Server Actions إلا عبر بروتوكول RSC Payload (لا يمكن محاكاتها باستخدام طلب HTTP POST عادي). يتحقق الإطار تلقائياً من تطابق أصل الطلب مع ملف تعريف الارتباط، لذلك لا حاجة لإضافة رمز CSRF يدوياً.
س هل يمكن لـ Server Action المضمنة الإشارة إلى props المكون؟
ج لا. 'use server' المضمنة هي دالة وحدة مستقلة موسومة في وقت التجميع — لا يمكنها الوصول إلى المتغيرات داخل closure. إذا كنت بحاجة إلى استخدام props أو حالة المكون، يجب تمريرها عبر formData أو تعريفها في ملف actions.ts منفصل.
س هل يمكن أن تكون معاملات Server Action من أي نوع؟
ج يجب أن تكون معاملات Server Action قابلة للتسلسل (وفقاً لقواعد RSC Props). FormData والكائنات العادية والمصفوفات والسلاسل النصية والأرقام مدعومة. الدوال و Date و undefined و Symbol غير مدعومة. إذا كنت بحاجة إلى معاملات معقدة، مررها عبر formData أو تسلسل JSON.
س كيف يسترجع العميل قيمة الإرجاع لـ Server Action؟
ج تُسلسل قيمة الإرجاع مرة أخرى إلى العميل عبر RSC Payload. في <form action={action}>، يتم تجاهل قيمة الإرجاع. لاسترداد قيمة الإرجاع، يجب استخدام useActionState(action, initialState) أو startTransition + callAction() صريح. نوصي باستخدام useActionState لإدارة قيم الإرجاع.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (⭐): أنشئ لوحة رسائل app/guestbook/page.tsx. استخدم Server Action مضمنة لمعالجة إرسال النموذج، وأخرج محتوى الرسالة باستخدام console.log (لمحاكاة الكتابة إلى قاعدة البيانات)، وقم بتحديث الصفحة revalidatePath بعد الإرسال.

  2. تمرين متقدم (⭐⭐): نفذ نظام إدارة مهام مع تحقق Zod في app/todos-zod/page.tsx. متطلبات المخطط: العنوان (1-100 حرف)، الأولوية (تعداد: low/medium/high)، تاريخ الاستحقاق (اختياري، تنسيق YYYY-MM-DD). اعرض أخطاء التحقق على مستوى الحقل أسفل النموذج.

  3. تحدٍّ (⭐⭐⭐): أنشئ وحدة "إنشاء مشروع" كاملة: app/projects/create/page.tsx تتضمن نموذجاً متعدد الحقول (الاسم، الوصف، تاريخ الاستحقاق، قائمة أعضاء الفريق)؛ عرِّف Server Actions لـ CRUD في app/actions/projects.ts؛ استخدم useActionState لإدارة حالة الإرسال (نجاح/خطأ/قيد الانتظار) على جانب العميل؛ وادعم إدخال رابط الصورة والمعاينة.

Web-Tutorial.com

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

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

100%