تطوير الويب 7 دقيقة للقراءة 2,573 مشاهدات

مكوّنات خادم React في الإنتاج: الحدّ الفاصل هو كل شيء

مكوّنات خادم React عمليًا — ما يعنيه "use client" فعلًا، ونمط التركيب الذي يمنع تحوّل الشجرة كلها إلى شيفرة عميل، وحدود التسلسل، ومتى تتجنبها.

React development illustration

تكاد كل مشكلة في مكوّنات خادم React أن تكون المشكلة نفسها بقناع مختلف: الحدّ الفاصل للعميل في المكان الخطأ. فإن ضبطته كانت المكوّنات مباشرة وسهلة، وإن أخطأته تكون قد شحنت تطبيق عميل عاديًا بخطوات إضافية.

وهذا الدليل عن ذلك الحدّ. الإصدارات وقت الكتابة: React 19.2 وNext.js 16.

ما الذي تلغيه فعلًا

مكوّن الخادم يعمل على الخادم ولا يصل المتصفح أبدًا: لا شيفرته ولا اعتمادياته.

// app/posts/page.tsx — مكوّن خادم افتراضيًا
import { marked } from 'marked'          // لا يُرسَل إلى المتصفح
import { db } from '@/lib/db'            // لا يُرسَل إلى المتصفح

export default async function PostsPage() {
  const posts = await db.post.findMany({ take: 20 })

  return (
    <ul>
      {posts.map(p => (
        <li key={p.id} dangerouslySetInnerHTML={{ __html: marked(p.body) }} />
      ))}
    </ul>
  )
}

واستيراد marked هو المقصود. ففي تطبيق React تقليدي يكون محلّل Markdown جزءًا من حزمتك ويُنزّله كل زائر، أما هنا فيعمل على الخادم ويستقبل المتصفح HTML.

والفائدة ليست توفير كيلوبايتات، بل أن أصنافًا كاملة من الاعتماديات تكفّ عن كونها مشكلة مستخدميك.

القاعدة التي لا يشرحها أحد جيدًا

'use client' لا تعلّم مكوّنًا واحدًا بأنه من جهة العميل، بل تعلّم نقطة دخول، وكل ما يُستورَد تحتها يصبح شيفرة عميل أيضًا.

// ❌ هذا يحوّل الصفحة كلها إلى مكوّن عميل
'use client'

import Chart from './Chart'          // صار عميلًا
import DataTable from './DataTable'  // صار عميلًا

export default function Dashboard({ data }) {
  const [tab, setTab] = useState('overview')   // السبب الوحيد لـ 'use client'

  return (
    <>
      <Tabs value={tab} onChange={setTab} />
      <Chart data={data} />
      <DataTable rows={data.rows} />
    </>
  )
}

فـ useState واحدة من أجل تبويب جرّت مكتبة الرسوم والجدول والأدوات إلى الحزمة. وهذا أشيع خطأ في المكوّنات، وهو غير مرئي: الصفحة تعمل تمامًا، لكنها لم تعد صفحة معروضة من الخادم بأي معنى حقيقي.

نمط التركيب الذي يصلح ذلك

// app/dashboard/page.tsx — يبقى مكوّن خادم
export default async function Dashboard() {
  const data = await getDashboardData()

  return (
    <Tabs>
      <Chart data={data} />
      <DataTable rows={data.rows} />
    </Tabs>
  )
}
// app/dashboard/Tabs.tsx
'use client'

export default function Tabs({ children }) {
  const [tab, setTab] = useState('overview')

  return (
    <div>
      <nav>{/* أزرار التبويب */}</nav>
      {children}          {/* مُعروض على الخادم مسبقًا */}
    </div>
  )
}

والقاعدة التي تستحق الحفظ: مكوّن العميل يستطيع عرض مكوّنات خادم تُمرَّر إليه كأبناء، لكنه لا يستطيع استيرادها. فالأبناء يصلون كمخرجات معروضة سلفًا، فيتوقف الحدّ عند Tabs ولا تدخل مكتبة الرسوم الحزمة أبدًا.

جلب البيانات والشلال

// ❌ متسلسل — كل انتظار يحجب التالي
const user = await getUser(id)
const orders = await getOrders(id)
const recommendations = await getRecommendations(id)

// ✅ متوازٍ — بزمن رحلة واحدة
const [user, orders, recommendations] = await Promise.all([
  getUser(id),
  getOrders(id),
  getRecommendations(id),
])

فالطلبات المستقلة ينبغي أن تكون دائمًا داخل Promise.all. ويسهل إغفال ذلك لأن النسخة المتسلسلة أسهل قراءةً وتتصرف بشكل مطابق أمام قاعدة بيانات محلية سريعة.

<ProductDetail id={id} />               {/* سريع، يُعرض فورًا */}
<Suspense fallback={<ReviewsSkeleton />}>
  <Reviews id={id} />                   {/* بطيء، يُبثّ لاحقًا */}
</Suspense>

ما الذي يعبر الحدّ

يعبرلا يعبر
النصوص والأرقام والقيم المنطقيةالدوال (عدا إجراءات الخادم)
المصفوفات والكائنات البسيطةنسخ الأصناف
Date و Map و Setالرموز
الوعود‏JSX من استيراد عميل

وصف نسخ الأصناف هو الأشد إيلامًا. فنموذج ORM يبدو غالبًا كائنًا بسيطًا لكنه يحمل دوالّ، فيفشل تمريره مباشرة إلى مكوّن عميل. حوّله إلى كائن بسيط أولًا — وهي ممارسة جيدة بذاتها، إذ تمنع تسرّب مخطط قاعدة بياناتك إلى خصائص واجهتك.

إجراءات الخادم

'use server'

export async function deletePost(formData: FormData) {
  const session = await auth()
  if (!session) throw new Error('Unauthorized')

  const id = String(formData.get('id'))
  const post = await db.post.findUnique({ where: { id } })

  if (post?.authorId !== session.userId) throw new Error('Forbidden')

  await db.post.delete({ where: { id } })
  revalidatePath('/posts')
}

ولاحظ الفحصين معًا. فإجراء الخادم نقطة نهاية HTTP عامة: يستطيع أي أحد استدعاءها بأي وسائط بغض النظر عما تعرضه واجهتك. فالمصادقة والصلاحيات مكانها داخل الإجراء لا في المكوّن الذي يعرض الزر. وإخفاء زر الحذف ليس تحكمًا بالوصول.

متى لا تستخدمها

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

كيف تعرف أنها تعمل

grep -rl "use client" app/ | wc -l
grep -rn "use client" app/ | head -20

فالتطبيق السليم يملك قليلًا منها قرب الأوراق: مربع بحث، أو قائمة، أو رسم بياني. أما إن حملت تخطيطًا أو صفحة عليا واحدة منها، فكل ما تحتها شيفرة عميل وتكون قد خسرت الفائدة دون أن تلاحظ.

أسئلة شائعة

ماذا تفعل "use client" فعلًا؟

تعلّم نقطة دخول إلى شيفرة العميل لا مكوّنًا واحدًا. فكل ما يُستورَد تحتها يصبح جزءًا من حزمة العميل، ولهذا يحوّل وضعها عاليًا في الشجرة صفحةً كاملة إلى تطبيق عميل بصمت.

هل يستطيع مكوّن عميل عرض مكوّن خادم؟

نعم إن مُرِّر مكوّن الخادم كأبناء أو كخاصية أخرى، لكنه لا يستطيع استيراده. وهذا التمييز هو ما يجعل نمط التركيب يعمل ويبقي الحدّ سطحيًا.

لماذا يظهر خطأ تسلسل عند تمرير الخصائص؟

لأن القيم القابلة للتسلسل وحدها تعبر الحدّ. فالدوال ونسخ الأصناف والرموز لا تعبر. ونماذج ORM هي المتهم المعتاد: تبدو كائنات بسيطة لكنها تحمل دوالّ، فحوّلها إلى كائنات بسيطة أولًا.

كيف أتجنّب شلالات جلب البيانات؟

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

هل إجراءات الخادم آمنة افتراضيًا؟

لا. فإجراء الخادم نقطة نهاية HTTP عامة يستطيع أي أحد استدعاءها بأي وسائط. والمصادقة والصلاحيات يجب أن تكون داخل الإجراء نفسه، وإخفاء الزر الذي يستدعيه ليس تحكمًا بالوصول.

هل تحل مكوّنات الخادم محل مسارات API؟

لبيانات واجهتك أنت نعم إلى حد بعيد، إذ تستطيع الاستعلام مباشرة في المكوّن. لكنك ما تزال تحتاج مسارات API لمستهلكي الطرف الثالث والخطافات الشبكية وعملاء الهاتف.

هل أرحّل تطبيق React الحالي إليها؟

فقط إن كان لديك سبب ملموس: حزمة كبيرة، أو أداء تحميل أول ضعيف، أو متطلبات SEO لا يلبّيها تطبيق عميل. أما ترحيل تطبيق سليم لذاته فمشروع كبير بعائد صغير.

كيف أعرف أنها تساعد فعلًا؟

عُدّ توجيهات 'use client' وتفقّد مواضعها. فقليلها قرب الأوراق علامة صحة، وواحدة في تخطيط علوي تعني أن كل ما تحتها شيفرة عميل — وحزمتك ستؤكد ذلك.

مشاركة هذه المقالة:
ES
كتبه

Edrees Salih

مهندس برمجيات متكامل يتمتع بخبرة 9 سنوات. شغوف ببناء حلول قابلة للتطوير ومشاركة المعرفة مع مجتمع المطورين.

عرض الملف الشخصي

التعليقات (0)

اترك تعليقًا

لن يتم نشر بريدك الإلكتروني.

لا توجد تعليقات بعد. كن أول من يشارك أفكاره!

مقالات ذات صلة

مقالات ذات صلة

هل تحتاج مساعدة في مشروعك؟

احجز استشارة مجانية لمدة 30 دقيقة لمناقشة تحدياتك التقنية واستكشاف الحلول معًا.