مكونات الخادم والعميل في Next.js App Router: دليل عملي شامل للأداء والتركيب

دليل عملي لمكونات الخادم والعميل في Next.js App Router يشمل أنماط التركيب المتقدمة كنمط الدونت والمزوّد، وجلب البيانات، وتوجيه use cache، وأفضل ممارسات الأداء والأمان مع أمثلة تطبيقية.

إذا كنت تعمل مع Next.js App Router، فمن المرجح أنك واجهت مفهوم مكونات الخادم (Server Components) ومكونات العميل (Client Components). صراحةً، عندما بدأت العمل مع هذا النموذج الجديد لأول مرة، شعرت بالارتباك — كل شيء يعمل على الخادم افتراضيًا؟ أين ذهبت مكونات React التي اعتدت عليها؟ لكن بعد فترة من التجربة والخطأ (والكثير من الأخطاء!)، أصبح الأمر أوضح بكثير. في هذا الدليل، سأشاركك كل ما تعلمته عن الفرق بين النوعين، وأنماط التركيب المتقدمة، وكيفية تحسين الأداء والأمان — بما في ذلك توجيه use cache والتصيير الجزئي المسبق (PPR) في Next.js 16.

ما هي مكونات الخادم في React ولماذا تُحدث فرقًا كبيرًا؟

مكونات الخادم في React (React Server Components أو RSC) هي مكونات تُنفَّذ حصريًا على الخادم. لا يُرسَل أي من شيفرتها البرمجية إلى المتصفح. هذا يعني أنها لا تُضاف إلى حزمة JavaScript التي يُحمّلها المستخدم، وبالتالي تقل حزمة JS بشكل ملحوظ ويتحسّن أداء التطبيق.

فكّر في الأمر: قبل ظهور مكونات الخادم، كان كل مكوّن React يُرسَل إلى المتصفح ويُنفَّذ هناك. حتى لو كان دوره الوحيد عرض بيانات ثابتة من قاعدة البيانات! كان هذا يعني إرسال مكتبات ضخمة مثل مكتبات تنسيق التواريخ أو معالجة Markdown إلى المتصفح دون أي داعٍ.

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

من أبرز المزايا:

  • الوصول المباشر لموارد الخادم: يمكنك الاستعلام من قاعدة البيانات مباشرة داخل المكوّن دون الحاجة لإنشاء واجهة API منفصلة.
  • تقليل حجم حزمة JavaScript: المكتبات المُستخدمة فقط على الخادم لا تُرسَل للمتصفح.
  • تحسين الأمان: المفاتيح السرية ورموز الوصول تبقى على الخادم ولا تُكشف أبدًا.
  • التدفق التلقائي: يمكن بث المحتوى تدريجيًا إلى المتصفح باستخدام Suspense.

فهم الفرق بين مكونات الخادم ومكونات العميل

الفرق الجوهري بين النوعين ليس فقط في مكان التنفيذ، بل في القدرات والقيود المفروضة على كل منهما. هيا نستعرض هذه الفروقات.

مكونات الخادم (Server Components)

مكونات الخادم تُنفَّذ على الخادم فقط ويُرسَل ناتجها (HTML و RSC Payload) إلى المتصفح. لا يمكنها استخدام أي واجهات برمجية خاصة بالمتصفح أو حالة تفاعلية.

// app/products/page.tsx
// This is a Server Component by default - no directive needed
import { db } from '@/lib/db'

export default async function ProductsPage() {
  // Direct database access - runs only on the server
  const products = await db.product.findMany({
    orderBy: { createdAt: 'desc' },
    take: 20,
  })

  return (
    <section>
      <h1>Our Products</h1>
      <ul>
        {products.map((product) => (
          <li key={product.id}>
            <h2>{product.name}</h2>
            <p>{product.description}</p>
            <span>${product.price}</span>
          </li>
        ))}
      </ul>
    </section>
  )
}

مكونات العميل (Client Components)

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

'use client'

// components/AddToCartButton.tsx
import { useState, useTransition } from 'react'

export default function AddToCartButton({ productId }: { productId: string }) {
  const [quantity, setQuantity] = useState(1)
  const [isPending, startTransition] = useTransition()

  const handleAddToCart = () => {
    startTransition(async () => {
      await fetch('/api/cart', {
        method: 'POST',
        body: JSON.stringify({ productId, quantity }),
      })
    })
  }

  return (
    <div>
      <input
        type="number"
        value={quantity}
        onChange={(e) => setQuantity(Number(e.target.value))}
        min={1}
      />
      <button onClick={handleAddToCart} disabled={isPending}>
        {isPending ? 'Adding...' : 'Add to Cart'}
      </button>
    </div>
  )
}

متى تستخدم كل نوع؟

لتسهيل اتخاذ القرار، إليك السيناريوهات المناسبة لكل نوع:

  • جلب البيانات من قاعدة البيانات أو API خارجية: مكوّن خادم
  • الوصول لموارد الخادم (ملفات النظام، متغيرات البيئة السرية): مكوّن خادم
  • عرض محتوى ثابت أو شبه ثابت: مكوّن خادم
  • استخدام مكتبات ثقيلة للتصيير فقط (مثل highlight.js أو marked): مكوّن خادم
  • التفاعل مع المستخدم (أزرار، نماذج تفاعلية): مكوّن عميل
  • استخدام useState أو useReducer: مكوّن عميل
  • استخدام useEffect أو مراجع DOM: مكوّن عميل
  • الاستماع لأحداث المتصفح (onClick، onChange): مكوّن عميل
  • استخدام واجهات المتصفح (localStorage، geolocation): مكوّن عميل
  • استخدام خطافات React المخصصة التي تعتمد على الحالة: مكوّن عميل

كيف تعمل مكونات الخادم من الداخل: حمولة RSC

دعني أخبرك بما يحدث خلف الكواليس — لأن فهم هذا الجزء سيُغيّر طريقة تفكيرك تمامًا. عندما يطلب المتصفح صفحة تحتوي على مكونات خادم:

  1. التنفيذ على الخادم: يُنفِّذ Next.js مكونات الخادم ويُحوّلها إلى تنسيق خاص يُسمى RSC Payload.
  2. إنشاء حمولة RSC: هذه الحمولة هي تمثيل تسلسلي (Serialized) لشجرة مكونات React، يحتوي على HTML المُصيَّر ومواقع مكونات العميل وخصائصها.
  3. دمج HTML: يستخدم Next.js حمولة RSC لإنشاء HTML على الخادم للتصيير الأولي السريع.
  4. الترطيب الانتقائي: في المتصفح، تُرطَّب مكونات العميل فقط (وليس مكونات الخادم)، مما يُقلّل من حجم JavaScript المطلوب.

حمولة RSC تحتوي على عدة أنواع من البيانات:

  • ناتج تصيير مكونات الخادم: شجرة React المُصيَّرة بالكامل.
  • عناصر نائبة لمكونات العميل: إشارات تُحدد أين يجب تصيير كل مكوّن عميل.
  • مراجع ملفات JavaScript: روابط لحزم JavaScript الخاصة بمكونات العميل.

النتيجة العملية؟ مكونات الخادم لا تحتاج لترطيب، فلا تُضيف أي شيء لحزمة JavaScript. تحميل أسرع وأداء أفضل — خصوصًا على الهواتف القديمة.

توجيه 'use client': كيف يعمل والأخطاء الشائعة

توجيه 'use client' هو البوابة التي تُحدد الحد الفاصل بين شيفرة الخادم وشيفرة العميل. عند وضعه في أعلى ملف، فإنه يُخبر Next.js أن هذا الملف وجميع الملفات التي يستوردها يجب أن تُضمَّن في حزمة JavaScript الخاصة بالعميل.

'use client'

// This directive must be at the top of the file
// Everything imported into this file becomes part of the client bundle

import { useState } from 'react'
import { format } from 'date-fns' // This library will be sent to the browser

export default function DatePicker() {
  const [selectedDate, setSelectedDate] = useState(new Date())

  return (
    <div>
      <p>Selected: {format(selectedDate, 'PPP')}</p>
      <input
        type="date"
        onChange={(e) => setSelectedDate(new Date(e.target.value))}
      />
    </div>
  )
}

أخطاء شائعة مع 'use client'

الخطأ الأول: وضع 'use client' في مكوّن الصفحة الرئيسي. وهذا — صدقني — من أكثر الأخطاء التي رأيتها تكرارًا. يجعل كل شيء في الصفحة مكوّن عميل ويُلغي فوائد مكونات الخادم تمامًا.

// BAD: Don't make the entire page a client component
'use client'

export default function ProductsPage() {
  // Now everything here is client-side, including data fetching
  // You lose all Server Component benefits
}
// GOOD: Keep the page as a Server Component
// Only extract interactive parts into separate client components
import { db } from '@/lib/db'
import SearchFilter from '@/components/SearchFilter' // 'use client' component

export default async function ProductsPage() {
  const products = await db.product.findMany()

  return (
    <div>
      <SearchFilter />
      <ProductList products={products} />
    </div>
  )
}

الخطأ الثاني: استيراد مكوّن خادم داخل مكوّن عميل. عند استيراد مكوّن داخل ملف يحمل توجيه 'use client'، يتحول ذلك المكوّن تلقائيًا إلى مكوّن عميل — حتى لو لم يكن يحتوي على التوجيه. وهذا بالضبط لماذا أنماط التركيب مهمة جدًا (سنتحدث عنها بالتفصيل لاحقًا).

الخطأ الثالث: تمرير كائنات غير قابلة للتسلسل كخصائص. الخصائص المُمرَّرة من مكونات الخادم إلى مكونات العميل يجب أن تكون قابلة للتسلسل. لا يمكنك تمرير دوال أو فئات أو عناصر DOM.

// BAD: Functions cannot be passed from Server to Client Components
export default function ServerPage() {
  const handleClick = () => console.log('clicked') // Not serializable

  return <ClientButton onClick={handleClick} /> // Error!
}

// GOOD: Use Server Actions for server-side functions
import { myServerAction } from '@/app/actions'

export default function ServerPage() {
  return <ClientButton action={myServerAction} /> // Server Actions are serializable
}

أنماط التركيب المتقدمة

والحقيقة أن أنماط التركيب (Composition Patterns) هي السر الحقيقي لبناء تطبيقات Next.js عالية الأداء. بدونها، ستجد نفسك تُحوّل كل شيء لمكوّن عميل بلا داعٍ.

نمط الدونت (The Donut Pattern)

هذا النمط هو المفضل لديّ شخصيًا. الفكرة بسيطة جدًا: بدلًا من استيراد مكوّن خادم داخل مكوّن عميل (مما يُحوّله لمكوّن عميل)، مرّره كـ children. بهذه الطريقة يحتفظ مكوّن الخادم بطبيعته حتى عند عرضه داخل مكوّن عميل.

// components/InteractiveWrapper.tsx
'use client'

import { useState } from 'react'

export default function InteractiveWrapper({
  children,
}: {
  children: React.ReactNode
}) {
  const [isExpanded, setIsExpanded] = useState(false)

  return (
    <div>
      <button onClick={() => setIsExpanded(!isExpanded)}>
        {isExpanded ? 'Collapse' : 'Expand'}
      </button>
      {isExpanded && <div className="content">{children}</div>}
    </div>
  )
}
// app/dashboard/page.tsx
// Server Component - no directive needed
import InteractiveWrapper from '@/components/InteractiveWrapper'
import { db } from '@/lib/db'

async function ServerDataDisplay() {
  const stats = await db.analytics.getStats()

  return (
    <div>
      <h3>Analytics Dashboard</h3>
      <p>Total Users: {stats.totalUsers}</p>
      <p>Revenue: ${stats.revenue}</p>
    </div>
  )
}

export default function DashboardPage() {
  return (
    <InteractiveWrapper>
      {/* ServerDataDisplay remains a Server Component */}
      {/* It is passed as children, not imported inside the client component */}
      <ServerDataDisplay />
    </InteractiveWrapper>
  )
}

لاحظ هنا: InteractiveWrapper مكوّن عميل يتحكم في إظهار/إخفاء المحتوى، لكن ServerDataDisplay يبقى مكوّن خادم يجلب البيانات من قاعدة البيانات مباشرة. السبب؟ لأنه مُمرَّر كـ children وليس مُستوردًا داخل ملف مكوّن العميل.

نمط المزوّد (Provider Pattern)

عند استخدام سياق React (Context) لإدارة الحالة العامة، تحتاج للف التطبيق بمكوّن عميل يعمل كمزوّد. الخبر الجيد هو أن هذا النمط يُحافظ على بقية التطبيق كمكونات خادم.

// providers/ThemeProvider.tsx
'use client'

import { createContext, useContext, useState, ReactNode } from 'react'

type Theme = 'light' | 'dark'

const ThemeContext = createContext<{
  theme: Theme
  toggleTheme: () => void
} | null>(null)

export function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<Theme>('light')

  const toggleTheme = () => {
    setTheme((prev) => (prev === 'light' ? 'dark' : 'light'))
  }

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      <div data-theme={theme}>{children}</div>
    </ThemeContext.Provider>
  )
}

export function useTheme() {
  const context = useContext(ThemeContext)
  if (!context) throw new Error('useTheme must be used within ThemeProvider')
  return context
}
// app/layout.tsx
// Server Component
import { ThemeProvider } from '@/providers/ThemeProvider'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="ar" dir="rtl">
      <body>
        <ThemeProvider>
          {/* All children can still be Server Components */}
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}

المفتاح هنا أن ThemeProvider مكوّن عميل، لكن children (محتوى الصفحات) يبقى مكونات خادم. لماذا؟ لأن React يُعالج children كخصائص مُمرَّرة وليس كمكونات مُستوردة.

نمط ترطيب المخزن (Store Hydration Pattern)

عند استخدام مكتبات إدارة الحالة مثل Zustand أو Jotai، قد تحتاج لتمرير بيانات أولية من الخادم إلى المخزن. هذا النمط يُتيح لك ذلك بسلاسة.

// stores/userStore.ts
import { create } from 'zustand'

interface UserState {
  user: { id: string; name: string; email: string } | null
  setUser: (user: UserState['user']) => void
}

export const useUserStore = create<UserState>((set) => ({
  user: null,
  setUser: (user) => set({ user }),
}))
// components/StoreHydrator.tsx
'use client'

import { useRef } from 'react'
import { useUserStore } from '@/stores/userStore'

export default function StoreHydrator({
  user,
  children,
}: {
  user: { id: string; name: string; email: string } | null
  children: React.ReactNode
}) {
  const initialized = useRef(false)

  if (!initialized.current) {
    useUserStore.setState({ user })
    initialized.current = true
  }

  return <>{children}</>
}
// app/layout.tsx
// Server Component
import { getUser } from '@/lib/auth'
import StoreHydrator from '@/components/StoreHydrator'

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const user = await getUser() // Fetch user on the server

  return (
    <html lang="ar" dir="rtl">
      <body>
        <StoreHydrator user={user}>
          {children}
        </StoreHydrator>
      </body>
    </html>
  )
}

جلب البيانات في مكونات الخادم

صراحةً، هذه من أجمل مزايا مكونات الخادم — القدرة على جلب البيانات مباشرة داخل المكوّن. هذا يُعرف بمبدأ التوطين المشترك (Co-location)، حيث يكون طلب البيانات في نفس المكان الذي يُستخدم فيه.

التوطين المشترك (Co-location)

// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'
import { db } from '@/lib/db'

export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await db.post.findUnique({ where: { slug } })

  if (!post) notFound()

  return (
    <article>
      <h1>{post.title}</h1>
      <time>{new Date(post.createdAt).toLocaleDateString('ar-SA')}</time>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

الجلب المتوازي (Parallel Fetching)

عندما تحتاج لجلب عدة مصادر بيانات، لا ترتكب خطأ جلبها بالتتابع! استخدم Promise.all لتنفيذها بالتوازي:

// app/dashboard/page.tsx
import { db } from '@/lib/db'

async function getStats() {
  return db.analytics.aggregate({ _count: true })
}

async function getRecentOrders() {
  return db.order.findMany({ take: 10, orderBy: { createdAt: 'desc' } })
}

async function getTopProducts() {
  return db.product.findMany({ take: 5, orderBy: { sales: 'desc' } })
}

export default async function DashboardPage() {
  // Parallel fetching - all three queries run simultaneously
  const [stats, recentOrders, topProducts] = await Promise.all([
    getStats(),
    getRecentOrders(),
    getTopProducts(),
  ])

  return (
    <div>
      <StatsCard stats={stats} />
      <OrdersTable orders={recentOrders} />
      <TopProductsList products={topProducts} />
    </div>
  )
}

إزالة التكرار باستخدام React.cache

هل تحتاج لنفس البيانات في أكثر من مكوّن؟ لا تقلق. React.cache يضمن تنفيذ الاستعلام مرة واحدة فقط لكل طلب:

// lib/data.ts
import { cache } from 'react'
import { db } from '@/lib/db'

// This function will only execute once per request,
// even if called from multiple components
export const getCurrentUser = cache(async () => {
  const session = await getSession()
  if (!session?.userId) return null

  return db.user.findUnique({
    where: { id: session.userId },
    include: { profile: true },
  })
})
// app/layout.tsx - calls getCurrentUser()
import { getCurrentUser } from '@/lib/data'
import Navbar from '@/components/Navbar'

export default async function Layout({ children }: { children: React.ReactNode }) {
  const user = await getCurrentUser() // First call - executes the query

  return (
    <div>
      <Navbar user={user} />
      {children}
    </div>
  )
}

// app/dashboard/page.tsx - also calls getCurrentUser()
import { getCurrentUser } from '@/lib/data'

export default async function DashboardPage() {
  const user = await getCurrentUser() // Second call - returns cached result

  return <h1>Welcome, {user?.name}</h1>
}

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

توجيه use cache والتصيير الجزئي المسبق (PPR) في Next.js 16

أضاف Next.js 16 نموذجًا جديدًا للتخزين المؤقت يعتمد على توجيه "use cache" — وصراحةً، هو أبسط بكثير من نظام التخزين المؤقت القديم. يعمل جنبًا إلى جنب مع التصيير الجزئي المسبق (PPR) لتقديم تجربة مستخدم سريعة جدًا.

تفعيل التخزين المؤقت للمكونات

أولًا، تحتاج لتفعيل الميزة التجريبية:

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  experimental: {
    dynamicIO: true,
    cacheLife: {
      // Custom cache profiles
      blog: {
        stale: 3600,    // 1 hour - serve stale content
        revalidate: 900, // 15 minutes - revalidate in background
        expire: 86400,   // 24 hours - max cache lifetime
      },
    },
  },
}

export default nextConfig

استخدام use cache في المكونات والدوال

يمكنك استخدام "use cache" على مستوى الصفحة بأكملها أو على مستوى دالة محددة:

// app/blog/page.tsx
import { unstable_cacheLife as cacheLife } from 'next/cache'

export default async function BlogPage() {
  "use cache"
  cacheLife('blog') // Use the custom cache profile defined in next.config.ts

  const posts = await db.post.findMany({
    orderBy: { publishedAt: 'desc' },
    take: 20,
  })

  return (
    <div>
      <h1>Blog</h1>
      {posts.map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  )
}
// lib/cached-data.ts
import {
  unstable_cacheLife as cacheLife,
  unstable_cacheTag as cacheTag,
} from 'next/cache'

export async function getCachedProducts(category: string) {
  "use cache"
  cacheLife('hours')
  cacheTag(`products-${category}`)

  const products = await db.product.findMany({
    where: { category },
    orderBy: { createdAt: 'desc' },
  })

  return products
}

مساعدات cacheLife و cacheTag

يوفر Next.js 16 مساعدات مدمجة للتحكم في سلوك التخزين المؤقت:

  • cacheLife: يُحدد مدة التخزين المؤقت. يمكنك استخدام ملفات تعريف مُسبقة مثل 'minutes' و 'hours' و 'days' و 'weeks' و 'max'، أو تعريف ملفات مخصصة في next.config.ts.
  • cacheTag: يُضيف وسومًا للمحتوى المُخزَّن مؤقتًا، مما يُتيح إبطاله بشكل انتقائي عند تغيّر البيانات.
// app/actions/revalidate.ts
'use server'

import { revalidateTag } from 'next/cache'

export async function updateProduct(productId: string, data: FormData) {
  await db.product.update({
    where: { id: productId },
    data: { name: data.get('name') as string },
  })

  // Invalidate all cached data tagged with this product's category
  revalidateTag(`products-${data.get('category')}`)
}

الجمع بين use cache و Suspense

هنا يظهر السحر الحقيقي. عند دمج "use cache" مع Suspense والتصيير الجزئي المسبق، يمكنك تقديم هيكل HTML ثابت فورًا مع بث المحتوى الديناميكي تدريجيًا:

// app/store/page.tsx
import { Suspense } from 'react'

// Static shell - rendered at build time
export default function StorePage() {
  return (
    <div>
      <h1>Our Store</h1>
      <StaticBanner />

      {/* Dynamic content streamed after initial load */}
      <Suspense fallback={<ProductsSkeleton />}>
        <FeaturedProducts />
      </Suspense>

      <Suspense fallback={<ReviewsSkeleton />}>
        <RecentReviews />
      </Suspense>
    </div>
  )
}

// This component uses cache for faster repeated loads
async function FeaturedProducts() {
  "use cache"
  cacheLife('hours')

  const products = await db.product.findMany({
    where: { featured: true },
    take: 8,
  })

  return (
    <section>
      <h2>Featured Products</h2>
      {products.map((p) => (
        <ProductCard key={p.id} product={p} />
      ))}
    </section>
  )
}

مع PPR، يُنشئ Next.js هيكل HTML ثابتًا يحتوي على الأجزاء غير الديناميكية (العنوان والشعار مثلًا)، ثم يبث الأجزاء الديناميكية بمجرد أن تكون جاهزة. النتيجة: سرعة الصفحات الثابتة مع مرونة المحتوى الديناميكي.

نصائح لتحسين الأداء

هيا نتحدث عن الأداء بشكل عملي. هذه الاستراتيجيات ستُحدث فرقًا حقيقيًا في تطبيقك:

تقليل حزمة JavaScript للعميل

القاعدة الذهبية: حافظ على أكبر قدر ممكن من الشيفرة على الخادم. كل مكوّن تُحوّله لمكوّن عميل يُضيف لحجم الحزمة.

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

تقسيم الشيفرة باستخدام next/dynamic

للمكونات التي لا تحتاجها فورًا عند تحميل الصفحة، استخدم التحميل الكسول مع next/dynamic:

// app/editor/page.tsx
import dynamic from 'next/dynamic'

// Heavy editor component - only loaded when needed
const RichTextEditor = dynamic(() => import('@/components/RichTextEditor'), {
  loading: () => <div className="skeleton-editor">Loading editor...</div>,
  ssr: false, // Don't render on the server if it requires browser APIs
})

// Heavy chart library - loaded on demand
const AnalyticsChart = dynamic(() => import('@/components/AnalyticsChart'), {
  loading: () => <div className="skeleton-chart">Loading chart...</div>,
})

export default function EditorPage() {
  return (
    <div>
      <h1>Content Editor</h1>
      <RichTextEditor />
      <AnalyticsChart />
    </div>
  )
}

استخدام Suspense للتدفق

قسّم صفحاتك لأقسام مُغلَّفة بـ Suspense لعرض المحتوى تدريجيًا. المستخدم سيرى المحتوى جزءًا بعد جزء بدلًا من شاشة تحميل واحدة طويلة:

// app/profile/page.tsx
import { Suspense } from 'react'

export default function ProfilePage() {
  return (
    <div>
      {/* Fast - shows immediately */}
      <Suspense fallback={<HeaderSkeleton />}>
        <UserHeader />
      </Suspense>

      {/* Medium - shows when ready */}
      <Suspense fallback={<ActivitySkeleton />}>
        <RecentActivity />
      </Suspense>

      {/* Slow - shows last */}
      <Suspense fallback={<RecommendationsSkeleton />}>
        <Recommendations />
      </Suspense>
    </div>
  )
}

كل قسم يُصيَّر بشكل مستقل ويُبث للمتصفح بمجرد أن يكون جاهزًا. تجربة مستخدم أفضل بكثير.

أفضل ممارسات الأمان

مكونات الخادم تُقدّم فوائد أمنية كبيرة. لكن — وهذا مهم — لا تزال بحاجة للانتباه لبعض الأمور.

الاحتفاظ بالأسرار على الخادم

المفاتيح السرية ورموز الوصول يجب أن تُستخدم فقط في مكونات الخادم أو Server Actions. لا تُمررها أبدًا كخصائص لمكونات العميل.

// GOOD: Secret stays on the server
// app/api-data/page.tsx (Server Component)
export default async function ApiDataPage() {
  const data = await fetch('https://api.example.com/data', {
    headers: {
      Authorization: `Bearer ${process.env.API_SECRET_KEY}`, // Server only
    },
  })

  const result = await data.json()

  return <DataDisplay data={result} /> // Only send the result, not the key
}
// BAD: Never expose secrets to client components
'use client'

export default function ClientFetcher() {
  const fetchData = async () => {
    // DANGER: This environment variable would need NEXT_PUBLIC_ prefix
    // to work in client, which means it's exposed to the browser!
    const res = await fetch('/api/data', {
      headers: { Authorization: `Bearer ${process.env.API_SECRET_KEY}` },
    })
  }
}

ولحماية إضافية، استخدم حزمة server-only لمنع استيراد ملفات الخادم في مكونات العميل عن طريق الخطأ:

// lib/server-utils.ts
import 'server-only' // Will throw a build error if imported in a client component

export async function getSecretData() {
  const apiKey = process.env.SECRET_API_KEY
  // This function can never accidentally end up in the client bundle
  return fetch('https://api.example.com/secret', {
    headers: { Authorization: `Bearer ${apiKey}` },
  })
}

التحقق من المدخلات في Server Actions

نقطة مهمة جدًا: Server Actions هي نقاط نهاية HTTP عامة. أي شخص يمكنه إرسال طلبات إليها مباشرة. لذا يجب التحقق من المدخلات والتصاريح دائمًا:

// app/actions/posts.ts
'use server'

import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { revalidatePath } from 'next/cache'

const CreatePostSchema = z.object({
  title: z.string().min(3).max(200),
  content: z.string().min(10).max(50000),
  category: z.enum(['tech', 'lifestyle', 'news']),
})

export async function createPost(formData: FormData) {
  // 1. Authentication - verify the user is logged in
  const session = await getSession()
  if (!session?.userId) {
    throw new Error('Unauthorized')
  }

  // 2. Input validation - never trust client data
  const rawData = {
    title: formData.get('title'),
    content: formData.get('content'),
    category: formData.get('category'),
  }

  const validatedData = CreatePostSchema.safeParse(rawData)

  if (!validatedData.success) {
    return {
      error: 'Invalid data',
      details: validatedData.error.flatten().fieldErrors,
    }
  }

  // 3. Authorization - verify the user has permission
  const user = await db.user.findUnique({ where: { id: session.userId } })
  if (!user || user.role !== 'author') {
    throw new Error('Forbidden')
  }

  // 4. Create the post with validated and sanitized data
  await db.post.create({
    data: {
      ...validatedData.data,
      authorId: session.userId,
    },
  })

  revalidatePath('/blog')
}

منع تسرب البيانات

عند تمرير بيانات من مكوّن خادم لمكوّن عميل، تأكد من تمرير فقط البيانات الضرورية:

// BAD: Passing entire user object with sensitive fields
export default async function ProfilePage() {
  const user = await db.user.findUnique({ where: { id: userId } })
  // user might contain: passwordHash, internalNotes, ssn, etc.
  return <ProfileCard user={user} /> // All data sent to client!
}

// GOOD: Only pass necessary fields
export default async function ProfilePage() {
  const user = await db.user.findUnique({
    where: { id: userId },
    select: {
      name: true,
      email: true,
      avatar: true,
      // Only select fields that should be visible to the client
    },
  })

  return <ProfileCard user={user} />
}

الأخطاء الشائعة وكيفية تجنبها

بعد سنوات من العمل مع مكونات الخادم والعميل (وارتكاب معظم هذه الأخطاء بنفسي!)، إليك أهم الأنماط المتكررة التي يقع فيها المطورون:

الخطأ 1: جعل كل شيء مكوّن عميل

هذا أكثر خطأ شائع. عندما تواجه خطأ مثل "useState is not defined"، الحل ليس إضافة 'use client' على كل شيء! استخرج فقط الجزء التفاعلي لمكوّن عميل منفصل.

// BAD: Making the entire page client-side to use one hook
'use client'

import { useState } from 'react'
import { HeavyMarkdownLib } from 'heavy-lib' // Now sent to client bundle!

export default function ArticlePage({ article }) {
  const [likes, setLikes] = useState(0)

  return (
    <div>
      <h1>{article.title}</h1>
      <div>{HeavyMarkdownLib.render(article.content)}</div>
      <button onClick={() => setLikes(l => l + 1)}>
        Like ({likes})
      </button>
    </div>
  )
}
// GOOD: Extract only the interactive part
// components/LikeButton.tsx
'use client'
import { useState } from 'react'

export function LikeButton() {
  const [likes, setLikes] = useState(0)
  return (
    <button onClick={() => setLikes(l => l + 1)}>
      Like ({likes})
    </button>
  )
}

// app/article/page.tsx (Server Component)
import { HeavyMarkdownLib } from 'heavy-lib' // Stays on server!
import { LikeButton } from '@/components/LikeButton'

export default function ArticlePage({ article }) {
  return (
    <div>
      <h1>{article.title}</h1>
      <div>{HeavyMarkdownLib.render(article.content)}</div>
      <LikeButton />
    </div>
  )
}

الخطأ 2: استيراد مكوّن خادم داخل مكوّن عميل

كما ذكرنا، عند استيراد مكوّن داخل ملف يحمل 'use client'، يتحول لمكوّن عميل. الحل؟ نمط الدونت:

// BAD: ServerComponent becomes a client component when imported here
'use client'

import ServerComponent from './ServerComponent'

export default function ClientWrapper() {
  return <ServerComponent /> // This is now a client component!
}

// GOOD: Pass as children instead
'use client'

export default function ClientWrapper({ children }: { children: React.ReactNode }) {
  return <div className="wrapper">{children}</div>
}

// In a Server Component parent:
// <ClientWrapper>
//   <ServerComponent />  {/* Stays a Server Component */}
// </ClientWrapper>

الخطأ 3: استخدام async/await في مكونات العميل

مكونات العميل لا يمكن أن تكون async. إذا كنت تحتاج لجلب بيانات، استخدم useEffect أو مكتبات مثل SWR و TanStack Query. أو الأفضل: اجلب البيانات في مكوّن خادم أب ومررها كخصائص.

// BAD: Client components cannot be async
'use client'

export default async function UserProfile() { // Error!
  const user = await fetchUser()
  return <div>{user.name}</div>
}

// GOOD: Fetch in parent Server Component, pass as props
// app/profile/page.tsx (Server Component)
import UserProfile from '@/components/UserProfile'

export default async function ProfilePage() {
  const user = await fetchUser() // Fetch on the server
  return <UserProfile user={user} /> // Pass data as props
}

// components/UserProfile.tsx (Client Component)
'use client'
import { useState } from 'react'

export default function UserProfile({ user }: { user: User }) {
  const [isEditing, setIsEditing] = useState(false)
  return (
    <div>
      <h2>{user.name}</h2>
      <button onClick={() => setIsEditing(!isEditing)}>Edit</button>
    </div>
  )
}

الخطأ 4: إهمال حدود الخادم والعميل

عند التنقل بين الصفحات في Next.js، لا تُعاد مكونات العميل من الصفر إذا كانت في التخطيط المشترك. حالتها تُحفظ بين التنقلات! كن واعيًا لهذا السلوك عند تصميم مكوناتك.

الخطأ 5: عدم التعامل مع حالات التحميل

عند استخدام مكونات خادم غير متزامنة، يجب دائمًا توفير حالة تحميل باستخدام Suspense أو ملفات loading.tsx. بدون ذلك، سيرى المستخدم شاشة فارغة أثناء تحميل البيانات — وهذا أسوأ من أي skeleton loader.

// app/products/loading.tsx
export default function ProductsLoading() {
  return (
    <div className="grid grid-cols-3 gap-4">
      {Array.from({ length: 6 }).map((_, i) => (
        <div key={i} className="animate-pulse">
          <div className="bg-gray-200 h-48 rounded" />
          <div className="bg-gray-200 h-4 mt-2 rounded w-3/4" />
          <div className="bg-gray-200 h-4 mt-1 rounded w-1/2" />
        </div>
      ))}
    </div>
  )
}

الخطأ 6: استخدام متغيرات البيئة بشكل خاطئ

في Next.js، فقط المتغيرات التي تبدأ بـ NEXT_PUBLIC_ متاحة في مكونات العميل. أي متغير آخر متاح فقط على الخادم. استخدام process.env.SECRET_KEY في مكوّن عميل سيُعيد undefined ببساطة.

// .env
DATABASE_URL=postgresql://...     // Server only
API_SECRET=sk_live_...            // Server only
NEXT_PUBLIC_APP_URL=https://...   // Available everywhere
NEXT_PUBLIC_STRIPE_KEY=pk_live_.. // Available everywhere (public key only!)

خلاصة وأفكار ختامية

بعد هذه الرحلة الطويلة مع مكونات الخادم والعميل، دعني ألخص أهم ما يجب أن تأخذه معك:

  1. مكونات الخادم هي الافتراضي: في App Router، كل شيء مكوّن خادم ما لم تُصرّح بخلاف ذلك بـ 'use client'.
  2. ادفع حدود العميل للأسفل: اجعل مكونات العميل صغيرة ومُركّزة على التفاعلية فقط.
  3. استخدم أنماط التركيب: نمط الدونت ونمط المزوّد هما أصدقاؤك الأوفياء.
  4. استفد من جلب البيانات المُوطَّن: اجلب البيانات في نفس المكوّن الذي يعرضها، واستخدم React.cache لتجنب التكرار و Promise.all للجلب المتوازي.
  5. اعتمد use cache: توجيه "use cache" في Next.js 16 يُبسّط التخزين المؤقت بشكل كبير.
  6. لا تُهمل الأمان: تحقق من المدخلات والتصاريح في Server Actions، واستخدم server-only لحماية الشيفرة الحساسة.
  7. قسّم الشيفرة بذكاء: استخدم next/dynamic للمكونات الثقيلة و Suspense لبث المحتوى تدريجيًا.

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

نصيحتي الأخيرة: لا تنتظر حتى تفهم كل شيء نظريًا — ابدأ ببناء مشروع صغير واختبر هذه الأنماط بنفسك. ستجد أن الأمور تتضح أكثر عندما تواجه المشاكل الفعلية وتحلها. واستخدم أدوات مثل Lighthouse و Next.js Analytics لقياس التحسينات التي تحققها.

عن الكاتب Editorial Team

Our team of expert writers and editors.