أنماط تكوين المكونات من جانب الخادم والعميل

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

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

إليك ملخصًا سريعًا لحالات الاستخدام المختلفة لمكونات الخادم والعميل:

ما الذي تريد فعله؟مكون الخادممكون العميل
جلب البياناتCheck IconCross Icon
الوصول إلى موارد الخلفية (مباشرة)Check IconCross Icon
الاحتفاظ بمعلومات حساسة على الخادم (رموز الوصول، مفاتيح API، إلخ)Check IconCross Icon
الاحتفاظ بتبعيات كبيرة على الخادم / تقليل JavaScript على جانب العميلCheck IconCross Icon
إضافة تفاعلات ومستمعين للأحداث (onClick(), onChange(), إلخ)Cross IconCheck Icon
استخدام الحالة وتأثيرات دورة الحياة (useState(), useReducer(), useEffect(), إلخ)Cross IconCheck Icon
استخدام واجهات برمجة التطبيقات المتاحة فقط في المتصفحCross IconCheck Icon
استخدام خطافات مخصصة تعتمد على الحالة، التأثيرات، أو واجهات برمجة التطبيقات المتاحة فقط في المتصفحCross IconCheck Icon
استخدام مكونات React الكلاسيكيةCross IconCheck Icon

أنماط مكونات الخادم

قبل اختيار العرض على جانب العميل، قد ترغب في القيام ببعض الأعمال على الخادم مثل جلب البيانات أو الوصول إلى قاعدة البيانات أو خدمات الخلفية.

إليك بعض الأنماط الشائعة عند العمل مع مكونات الخادم:

مشاركة البيانات بين المكونات

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

بدلاً من استخدام سياق React (غير متاح على الخادم) أو تمرير البيانات كخصائص، يمكنك استخدام fetch أو دالة cache في React لجلب نفس البيانات في المكونات التي تحتاجها، دون القلق بشأن تكرار طلبات البيانات نفسها. وذلك لأن React يمتد fetch لتخزين طلبات البيانات تلقائيًا، ويمكن استخدام دالة cache عندما لا يكون fetch متاحًا.

تعلم المزيد عن التخزين المؤقت في React.

إبقاء كود الخادم فقط خارج بيئة العميل

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

على سبيل المثال، خذ دالة جلب البيانات التالية:

export async function getData() {
  const res = await fetch('https://external-service.com/data', {
    headers: {
      authorization: process.env.API_KEY,
    },
  })

  return res.json()
}
export async function getData() {
  const res = await fetch('https://external-service.com/data', {
    headers: {
      authorization: process.env.API_KEY,
    },
  })

  return res.json()
}

للوهلة الأولى، يبدو أن getData تعمل على كل من الخادم والعميل. ومع ذلك، تحتوي هذه الدالة على API_KEY، المكتوبة بنية أن يتم تنفيذها على الخادم فقط.

نظرًا لأن متغير البيئة API_KEY لا يبدأ بـ NEXT_PUBLIC، فهو متغير خاص لا يمكن الوصول إليه إلا على الخادم. لمنع تسرب متغيرات البيئة الخاصة إلى العميل، يستبدل Next.js متغيرات البيئة الخاصة بسلسلة فارغة.

نتيجة لذلك، على الرغم من أنه يمكن استيراد getData() وتنفيذها على العميل، إلا أنها لن تعمل كما هو متوقع. وبينما جعل المتغير عامًا سيجعل الدالة تعمل على العميل، قد لا ترغب في الكشف عن معلومات حساسة للعميل.

لمنع هذا النوع من الاستخدام غير المقصود لكود الخادم على العميل، يمكننا استخدام حزمة server-only لإعطاء مطورين آخرين خطأ في وقت البناء إذا قاموا باستيراد إحدى هذه الوحدات عن طريق الخطأ إلى مكون عميل.

لاستخدام server-only، قم أولاً بتثبيت الحزمة:

Terminal
npm install server-only

ثم استورد الحزمة في أي وحدة تحتوي على كود خاص بالخادم فقط:

lib/data.js
import 'server-only'

export async function getData() {
  const res = await fetch('https://external-service.com/data', {
    headers: {
      authorization: process.env.API_KEY,
    },
  })

  return res.json()
}

الآن، أي مكون عميل يستورد getData() سيتلقى خطأ في وقت البناء يوضح أن هذه الوحدة يمكن استخدامها فقط على الخادم.

يمكن استخدام الحزمة المقابلة client-only لوضع علامة على الوحدات التي تحتوي على كود خاص بالعميل فقط - على سبيل المثال، الكود الذي يصل إلى كائن window.

استخدام الحزم والموفّرين من طرف ثالث

نظرًا لأن مكونات الخادم هي ميزة جديدة في React، فإن الحزم والموفّرين من طرف ثالث في النظام البيئي بدأوا للتو في إضافة توجيه "use client" إلى المكونات التي تستخدم ميزات خاصة بالعميل مثل useState وuseEffect وcreateContext.

اليوم، العديد من المكونات من حزم npm التي تستخدم ميزات خاصة بالعميل لا تحتوي بعد على التوجيه. ستعمل هذه المكونات من طرف ثالث كما هو متوقع داخل مكونات العميل لأن لديها توجيه "use client"، لكنها لن تعمل داخل مكونات الخادم.

على سبيل المثال، لنفترض أنك قمت بتثبيت حزمة افتراضية acme-carousel التي تحتوي على مكون <Carousel />. يستخدم هذا المكون useState، لكنه لا يحتوي بعد على توجيه "use client".

إذا استخدمت <Carousel /> داخل مكون عميل، فسيعمل كما هو متوقع:

'use client'

import { useState } from 'react'
import { Carousel } from 'acme-carousel'

export default function Gallery() {
  let [isOpen, setIsOpen] = useState(false)

  return (
    <div>
      <button onClick={() => setIsOpen(true)}>View pictures</button>

      {/* Works, since Carousel is used within a Client Component */}
      {isOpen && <Carousel />}
    </div>
  )
}
'use client'

import { useState } from 'react'
import { Carousel } from 'acme-carousel'

export default function Gallery() {
  let [isOpen, setIsOpen] = useState(false)

  return (
    <div>
      <button onClick={() => setIsOpen(true)}>View pictures</button>

      {/*  Works, since Carousel is used within a Client Component */}
      {isOpen && <Carousel />}
    </div>
  )
}

ومع ذلك، إذا حاولت استخدامه مباشرة داخل مكون خادم، فسترى خطأ:

import { Carousel } from 'acme-carousel'

export default function Page() {
  return (
    <div>
      <p>View pictures</p>

      {/* Error: `useState` can not be used within Server Components */}
      <Carousel />
    </div>
  )
}
import { Carousel } from 'acme-carousel'

export default function Page() {
  return (
    <div>
      <p>View pictures</p>

      {/*  Error: `useState` can not be used within Server Components */}
      <Carousel />
    </div>
  )
}

هذا لأن Next.js لا يعرف أن <Carousel /> يستخدم ميزات خاصة بالعميل.

لإصلاح هذا، يمكنك تغليف المكونات من طرف ثالث التي تعتمد على ميزات خاصة بالعميل داخل مكونات العميل الخاصة بك:

'use client'

import { Carousel } from 'acme-carousel'

export default Carousel
'use client'

import { Carousel } from 'acme-carousel'

export default Carousel

الآن، يمكنك استخدام <Carousel /> مباشرة داخل مكون خادم:

import Carousel from './carousel'

export default function Page() {
  return (
    <div>
      <p>View pictures</p>

      {/*  Works, since Carousel is a Client Component */}
      <Carousel />
    </div>
  )
}
import Carousel from './carousel'

export default function Page() {
  return (
    <div>
      <p>View pictures</p>

      {/*  Works, since Carousel is a Client Component */}
      <Carousel />
    </div>
  )
}

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

استخدام موفّري السياق

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

import { createContext } from 'react'

//  createContext is not supported in Server Components
export const ThemeContext = createContext({})

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
      </body>
    </html>
  )
}
import { createContext } from 'react'

//  createContext is not supported in Server Components
export const ThemeContext = createContext({})

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
      </body>
    </html>
  )
}

لإصلاح هذا، قم بإنشاء سياقك وعرض موفّره داخل مكون عميل:

'use client'

import { createContext } from 'react'

export const ThemeContext = createContext({})

export default function ThemeProvider({ children }) {
  return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}
'use client'

import { createContext } from 'react'

export const ThemeContext = createContext({})

export default function ThemeProvider({ children }) {
  return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}

سيكون مكون الخادم الخاص بك قادرًا الآن على عرض موفّرك مباشرةً لأنه تم وضع علامة عليه كمكون عميل:

import ThemeProvider from './theme-provider'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  )
}
import ThemeProvider from './theme-provider'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  )
}

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

ملاحظة جيدة: يجب أن تعرض الموفّرين بأقصى عمق ممكن في الشجرة - لاحظ كيف أن ThemeProvider يلف فقط {children} بدلاً من مستند <html> بأكمله. هذا يجعل من السهل على Next.js تحسين الأجزاء الثابتة من مكونات الخادم الخاصة بك.

نصائح لمؤلفي المكتبات

بطريقة مماثلة، يمكن لمؤلفي المكتبات الذين ينشئون حزمًا ليتم استهلاكها من قبل مطورين آخرين استخدام توجيه "use client" لوضع علامة على نقاط دخول العميل في حزمتهم. هذا يسمح لمستخدمي الحزمة باستيراد مكونات الحزمة مباشرة إلى مكونات الخادم الخاصة بهم دون الحاجة إلى إنشاء حد تغليف.

يمكنك تحسين حزمتك باستخدام 'use client' بشكل أعمق في الشجرة، مما يسمح لوحدات الوارد أن تكون جزءًا من الرسم البياني لوحدة مكون الخادم.

من الجدير بالذكر أن بعض أدوات الحزم قد تزيل توجيهات "use client". يمكنك العثور على مثال لكيفية تكوين esbuild لتضمين توجيه "use client" في مستودعات React Wrap Balancer وVercel Analytics.

مكونات العميل

نقل مكونات العميل لأسفل الشجرة

لتقليل حجم حزمة JavaScript الخاصة بالعميل، نوصي بنقل مكونات العميل لأسفل شجرة المكونات الخاصة بك.

على سبيل المثال، قد يكون لديك تخطيط يحتوي على عناصر ثابتة (مثل الشعار، الروابط، إلخ) وشريط بحث تفاعلي يستخدم الحالة.

بدلاً من جعل التخطيط بأكمله مكون عميل، انقل المنطق التفاعلي إلى مكون عميل (مثل <SearchBar />) واحتفظ بتخطيطك كمكون خادم. هذا يعني أنك لست مضطرًا لإرسال جميع كود JavaScript الخاص بالمكونات في التخطيط إلى العميل.

// SearchBar هو مكون عميل
import SearchBar from './searchbar'
// Logo هو مكون خادم
import Logo from './logo'

// Layout هو مكون خادم افتراضيًا
export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <nav>
        <Logo />
        <SearchBar />
      </nav>
      <main>{children}</main>
    </>
  )
}
// SearchBar هو مكون عميل
import SearchBar from './searchbar'
// Logo هو مكون خادم
import Logo from './logo'

// Layout هو مكون خادم افتراضيًا
export default function Layout({ children }) {
  return (
    <>
      <nav>
        <Logo />
        <SearchBar />
      </nav>
      <main>{children}</main>
    </>
  )
}

تمرير الخصائص من الخادم إلى مكونات العميل (التسلسل)

إذا قمت بجلب البيانات في مكون خادم، فقد ترغب في تمرير البيانات كخصائص إلى مكونات العميل. يجب أن تكون الخصائص الممررة من الخادم إلى مكونات العميل قابلة للتسلسل بواسطة React.

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

تداخل مكونات الخادم والعميل

عند تداخل مكونات الخادم (Server Components) والعميل (Client Components)، قد يكون من المفيد تصوير واجهة المستخدم الخاصة بك كشجرة من المكونات. بدءًا من تخطيط الجذر وهو مكون خادم، يمكنك بعد ذلك عرض بعض الأشجار الفرعية للمكونات على جانب العميل عن طريق إضافة التوجيه "use client".

داخل أشجار العميل الفرعية هذه، لا يزال بإمكانك تضمين مكونات الخادم أو استدعاء إجراءات الخادم (Server Actions)، ولكن هناك بعض الأشياء التي يجب وضعها في الاعتبار:

  • خلال دورة حياة الطلب-الاستجابة، ينتقل الكود الخاص بك من الخادم إلى العميل. إذا كنت بحاجة إلى الوصول إلى بيانات أو موارد على الخادم أثناء وجودك على العميل، فستقوم بعمل طلب جديد إلى الخادم - وليس التبديل ذهابًا وإيابًا.
  • عند عمل طلب جديد إلى الخادم، يتم عرض جميع مكونات الخادم أولاً، بما في ذلك تلك المتداخلة داخل مكونات العميل. ستحتوي نتيجة العرض (RSC Payload) على إشارات إلى مواقع مكونات العميل. ثم، على العميل، يستخدم React حمولة RPC لتنسيق مكونات الخادم والعميل في شجرة واحدة.
  • نظرًا لأن مكونات العميل يتم عرضها بعد مكونات الخادم، لا يمكنك استيراد مكون خادم إلى وحدة مكون عميل (لأن ذلك سيتطلب طلبًا جديدًا إلى الخادم). بدلاً من ذلك، يمكنك تمرير مكون خادم كـ props إلى مكون عميل. راجع الأقسام النمط غير المدعوم والنمط المدعوم أدناه.

النمط غير المدعوم: استيراد مكونات الخادم إلى مكونات العميل

النمط التالي غير مدعوم. لا يمكنك استيراد مكون خادم إلى مكون عميل:

'use client'

// لا يمكنك استيراد مكون خادم إلى مكون عميل.
import ServerComponent from './Server-Component'

export default function ClientComponent({
  children,
}: {
  children: React.ReactNode
}) {
  const [count, setCount] = useState(0)

  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>

      <ServerComponent />
    </>
  )
}
'use client'

// لا يمكنك استيراد مكون خادم إلى مكون عميل.
import ServerComponent from './Server-Component'

export default function ClientComponent({ children }) {
  const [count, setCount] = useState(0)

  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>

      <ServerComponent />
    </>
  )
}

النمط المدعوم: تمرير مكونات الخادم إلى مكونات العميل كـ Props

النمط التالي مدعوم. يمكنك تمرير مكونات الخادم كـ prop إلى مكون عميل.

من الأنماط الشائعة استخدام خاصية React children لإنشاء "فتحة" في مكون العميل الخاص بك.

في المثال أدناه، <ClientComponent> يقبل خاصية children:

'use client'

import { useState } from 'react'

export default function ClientComponent({
  children,
}: {
  children: React.ReactNode
}) {
  const [count, setCount] = useState(0)

  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>
      {children}
    </>
  )
}
'use client'

import { useState } from 'react'

export default function ClientComponent({ children }) {
  const [count, setCount] = useState(0)

  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>

      {children}
    </>
  )
}

<ClientComponent> لا يعرف أن children سيتم ملؤها لاحقًا بنتيجة مكون خادم. المسؤولية الوحيدة لـ <ClientComponent> هي تحديد أين سيتم وضع children في النهاية.

في مكون خادم أب، يمكنك استيراد كل من <ClientComponent> و <ServerComponent> وتمرير <ServerComponent> كطفل لـ <ClientComponent>:

// هذا النمط يعمل:
// يمكنك تمرير مكون خادم كطفل أو prop لمكون عميل.
import ClientComponent from './client-component'
import ServerComponent from './server-component'

// الصفحات في Next.js هي مكونات خادم افتراضيًا
export default function Page() {
  return (
    <ClientComponent>
      <ServerComponent />
    </ClientComponent>
  )
}
// هذا النمط يعمل:
// يمكنك تمرير مكون خادم كطفل أو prop لمكون عميل.
import ClientComponent from './client-component'
import ServerComponent from './server-component'

// الصفحات في Next.js هي مكونات خادم افتراضيًا
export default function Page() {
  return (
    <ClientComponent>
      <ServerComponent />
    </ClientComponent>
  )
}

بهذه الطريقة، يتم فصل <ClientComponent> و <ServerComponent> ويمكن عرضهما بشكل مستقل. في هذه الحالة، يمكن عرض الطفل <ServerComponent> على الخادم، قبل وقت طويل من عرض <ClientComponent> على العميل.

معلومة جيدة:

  • تم استخدام نمط "رفع المحتوى لأعلى" لتجنب إعادة عرض مكون طفل متداخل عند إعادة عرض مكون أب.
  • لا تقتصر على خاصية children. يمكنك استخدام أي prop لتمرير JSX.