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

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

جلب البيانات على الخادم

كلما أمكن، نوصي بجلب البيانات على الخادم باستخدام مكونات الخادم (Server Components). هذا يسمح لك بـ:

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

ثم يمكنك تعديل أو تحديث البيانات باستخدام الإجراءات الخادمية (Server Actions).

جلب البيانات حيثما تكون مطلوبة

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

هذا ممكن لأن طلبات fetch يتم تذكرها تلقائيًا. تعلم المزيد عن تذكر الطلبات (request memoization)

معلومة مفيدة: هذا ينطبق أيضًا على التخطيطات (layouts)، لأنه لا يمكن تمرير البيانات بين التخطيط الأب وأطفاله.

البث (Streaming)

البث و التعليق (Suspense) هما ميزتان في React تسمحان لك بتصيير وحدات واجهة المستخدم تدريجيًا وبثها إلى العميل بشكل تدريجي.

مع مكونات الخادم و التخطيطات المتداخلة (nested layouts)، يمكنك تصيير أجزاء الصفحة التي لا تتطلب بيانات على الفور، وعرض حالة التحميل (loading state) للأجزاء التي تقوم بجلب البيانات. هذا يعني أن المستخدم لا يضطر إلى انتظار تحميل الصفحة بالكامل قبل أن يتمكن من التفاعل معها.

تصيير الخادم مع البث

للمزيد عن البث والتعليق، راجع صفحات واجهة التحميل (Loading UI) و البث مع التعليق (Streaming with Suspense).

جلب البيانات المتوازي والتسلسلي

عند جلب البيانات داخل مكونات React، يجب أن تكون على دراية بنمطين لجلب البيانات: المتوازي والتسلسلي.

جلب البيانات التسلسلي والمتوازي
  • مع جلب البيانات التسلسلي (sequential data fetching): تكون الطلبات في المسار معتمدة على بعضها البعض وبالتالي تخلق شلالات. قد تكون هناك حالات تريد فيها هذا النمط لأن أحد الطلبات يعتمد على نتيجة الآخر، أو لأنك تريد استيفاء شرط قبل الطلب التالي لتوفير الموارد. ومع ذلك، قد يكون هذا السلوك غير مقصود ويؤدي إلى أوقات تحميل أطول.
  • مع جلب البيانات المتوازي (parallel data fetching): يتم بدء الطلبات في المسار بفارغ الصبر وتحميل البيانات في نفس الوقت. هذا يقلل من شلالات العميل-الخادم والوقت الإجمالي لتحميل البيانات.

جلب البيانات التسلسلي

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

على سبيل المثال، لن يبدأ مكون Playlists في جلب البيانات إلا بعد انتهاء مكون Artist من جلب البيانات لأن Playlists يعتمد على خاصية artistID:

// ...

async function Playlists({ artistID }: { artistID: string }) {
  // انتظار قوائم التشغيل
  const playlists = await getArtistPlaylists(artistID)

  return (
    <ul>
      {playlists.map((playlist) => (
        <li key={playlist.id}>{playlist.name}</li>
      ))}
    </ul>
  )
}

export default async function Page({
  params: { username },
}: {
  params: { username: string }
}) {
  // انتظار الفنان
  const artist = await getArtist(username)

  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  )
}
// ...

async function Playlists({ artistID }) {
  // انتظار قوائم التشغيل
  const playlists = await getArtistPlaylists(artistID)

  return (
    <ul>
      {playlists.map((playlist) => (
        <li key={playlist.id}>{playlist.name}</li>
      ))}
    </ul>
  )
}

export default async function Page({ params: { username } }) {
  // انتظار الفنان
  const artist = await getArtist(username)

  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  )
}

في مثل هذه الحالات، يمكنك استخدام loading.js (لقطاعات المسار) أو React <Suspense> (للمكونات المتداخلة) لعرض حالة تحميل فورية أثناء قيام React ببث النتيجة.

هذا سيمنع حظر المسار بالكامل بسبب جلب البيانات، وسيتمكن المستخدم من التفاعل مع أجزاء الصفحة غير المحظورة.

طلبات البيانات المحظورة:

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

أي طلبات جلب مع await ستحظر التصيير وجلب البيانات للشجرة بأكملها تحتها، ما لم يتم تغليفها في حدود <Suspense> أو استخدام loading.js. نهج آخر هو استخدام جلب البيانات المتوازي أو نمط التحميل المسبق (preload pattern).

جلب البيانات المتوازي

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

في المثال أدناه، يتم تعريف الدالتين getArtist و getArtistAlbums خارج مكون Page، ثم يتم استدعاؤهما داخل المكون، وننتظر حل كلا الوعدين:

import Albums from './albums'

async function getArtist(username: string) {
  const res = await fetch(`https://api.example.com/artist/${username}`)
  return res.json()
}

async function getArtistAlbums(username: string) {
  const res = await fetch(`https://api.example.com/artist/${username}/albums`)
  return res.json()
}

export default async function Page({
  params: { username },
}: {
  params: { username: string }
}) {
  // بدء كلا الطلبين بشكل متوازي
  const artistData = getArtist(username)
  const albumsData = getArtistAlbums(username)

  // انتظار حل الوعدين
  const [artist, albums] = await Promise.all([artistData, albumsData])

  return (
    <>
      <h1>{artist.name}</h1>
      <Albums list={albums}></Albums>
    </>
  )
}
import Albums from './albums'

async function getArtist(username) {
  const res = await fetch(`https://api.example.com/artist/${username}`)
  return res.json()
}

async function getArtistAlbums(username) {
  const res = await fetch(`https://api.example.com/artist/${username}/albums`)
  return res.json()
}

export default async function Page({ params: { username } }) {
  // بدء كلا الطلبين بشكل متوازي
  const artistData = getArtist(username)
  const albumsData = getArtistAlbums(username)

  // انتظار حل الوعدين
  const [artist, albums] = await Promise.all([artistData, albumsData])

  return (
    <>
      <h1>{artist.name}</h1>
      <Albums list={albums}></Albums>
    </>
  )
}

لتحسين تجربة المستخدم، يمكنك إضافة حدود التعليق (Suspense Boundary) لتقسيم عمل التصيير وعرض جزء من النتيجة في أسرع وقت ممكن.

التحميل المسبق للبيانات

طريقة أخرى لمنع الشلالات هي استخدام نمط التحميل المسبق. يمكنك إنشاء دالة preload اختيارية لتحسين جلب البيانات المتوازي بشكل أكبر. مع هذا النهج، ليس عليك تمرير الوعود كخصائص. يمكن أن يكون لدالة preload أي اسم لأنها نمط وليس واجهة برمجة تطبيقات (API).

import { getItem } from '@/utils/get-item'

export const preload = (id: string) => {
  // void تقوم بتقييم التعبير المعطى وإرجاع undefined
  // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
  void getItem(id)
}
export default async function Item({ id }: { id: string }) {
  const result = await getItem(id)
  // ...
}
import { getItem } from '@/utils/get-item'

export const preload = (id) => {
  // void تقوم بتقييم التعبير المعطى وإرجاع undefined
  // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
  void getItem(id)
}
export default async function Item({ id }) {
  const result = await getItem(id)
  // ...
}
import Item, { preload, checkIsAvailable } from '@/components/Item'

export default async function Page({
  params: { id },
}: {
  params: { id: string }
}) {
  // بدء تحميل بيانات العنصر
  preload(id)
  // تنفيذ مهمة غير متزامنة أخرى
  const isAvailable = await checkIsAvailable()

  return isAvailable ? <Item id={id} /> : null
}
import Item, { preload, checkIsAvailable } from '@/components/Item'

export default async function Page({ params: { id } }) {
  // بدء تحميل بيانات العنصر
  preload(id)
  // تنفيذ مهمة غير متزامنة أخرى
  const isAvailable = await checkIsAvailable()

  return isAvailable ? <Item id={id} /> : null
}

استخدام cache من React، server-only، ونمط التحميل المسبق

يمكنك الجمع بين دالة cache، ونمط preload، وحزمة server-only لإنشاء أداة جلب بيانات يمكن استخدامها في جميع أنحاء تطبيقك.

import { cache } from 'react'
import 'server-only'

export const preload = (id: string) => {
  void getItem(id)
}

export const getItem = cache(async (id: string) => {
  // ...
})
import { cache } from 'react'
import 'server-only'

export const preload = (id) => {
  void getItem(id)
}

export const getItem = cache(async (id) => {
  // ...
})

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

يمكن استخدام صادرات utils/get-item بواسطة التخطيطات أو الصفحات أو المكونات الأخرى لمنحها التحكم في وقت جلب بيانات العنصر.

معلومة مفيدة:

  • نوصي باستخدام حزمة server-only للتأكد من عدم استخدام دوال جلب بيانات الخادم على العميل أبدًا.

منع تسرب البيانات الحساسة إلى العميل

نوصي باستخدام واجهات تلويث React، taintObjectReference و taintUniqueValue، لمنع تمرير كائنات كاملة أو قيم حساسة إلى العميل.

لتمكين التلويث في تطبيقك، اضبط خيار experimental.taint في إعدادات Next.js على true:

next.config.js
module.exports = {
  experimental: {
    taint: true,
  },
}

ثم مرر الكائن أو القيمة التي تريد تلويثها إلى دوال experimental_taintObjectReference أو experimental_taintUniqueValue:

import { queryDataFromDB } from './api'
import {
  experimental_taintObjectReference,
  experimental_taintUniqueValue,
} from 'react'

export async function getUserData() {
  const data = await queryDataFromDB()
  experimental_taintObjectReference(
    'لا تمرر كائن المستخدم بالكامل إلى العميل',
    data
  )
  experimental_taintUniqueValue(
    "لا تمرر عنوان المستخدم إلى العميل",
    data,
    data.address
  )
  return data
}
import { queryDataFromDB } from './api'
import {
  experimental_taintObjectReference,
  experimental_taintUniqueValue,
} from 'react'

export async function getUserData() {
  const data = await queryDataFromDB()
  experimental_taintObjectReference(
    'لا تمرر كائن المستخدم بالكامل إلى العميل',
    data
  )
  experimental_taintUniqueValue(
    "لا تمرر عنوان المستخدم إلى العميل",
    data,
    data.address
  )
  return data
}
import { getUserData } from './data'

export async function Page() {
  const userData = getUserData()
  return (
    <ClientComponent
      user={userData} // سيؤدي هذا إلى خطأ بسبب taintObjectReference
      address={userData.address} // سيؤدي هذا إلى خطأ بسبب taintUniqueValue
    />
  )
}
import { getUserData } from './data'

export async function Page() {
  const userData = await getUserData()
  return (
    <ClientComponent
      user={userData} // سيؤدي هذا إلى خطأ بسبب taintObjectReference
      address={userData.address} // سيؤدي هذا إلى خطأ بسبب taintUniqueValue
    />
  )
}

تعلم المزيد عن الأمان وإجراءات الخادم (Security and Server Actions).