كيفية جلب البيانات والتدفق

سترشدك هذه الصفحة حول كيفية جلب البيانات في مكونات الخادم والعميل (Server and Client Components)، وكيفية تدفق المكونات التي تعتمد على البيانات.

جلب البيانات

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

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

  1. واجهة fetch API
  2. ORM أو قاعدة بيانات

مع fetch API

لجلب البيانات باستخدام fetch API، حول مكونك إلى دالة غير متزامنة (async)، واستخدم await مع استدعاء fetch. على سبيل المثال:

export default async function Page() {
  const data = await fetch('https://api.vercel.app/blog')
  const posts = await data.json()
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}
export default async function Page() {
  const data = await fetch('https://api.vercel.app/blog')
  const posts = await data.json()
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

جيد أن تعرف:

مع ORM أو قاعدة بيانات

بما أن مكونات الخادم يتم تقديمها على الخادم، يمكنك بأمان إجراء استعلامات قاعدة بيانات باستخدام ORM أو عميل قاعدة بيانات. حول مكونك إلى دالة غير متزامنة واستخدم await مع الاستدعاء:

import { db, posts } from '@/lib/db'

export default async function Page() {
  const allPosts = await db.select().from(posts)
  return (
    <ul>
      {allPosts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}
import { db, posts } from '@/lib/db'

export default async function Page() {
  const allPosts = await db.select().from(posts)
  return (
    <ul>
      {allPosts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

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

هناك طريقتان لجلب البيانات في مكونات العميل، باستخدام:

  1. خطاف use في React
  2. مكتبة مجتمعية مثل SWR أو React Query

تدفق البيانات مع خطاف use

يمكنك استخدام خطاف use في React لتدفق البيانات من الخادم إلى العميل. ابدأ بجلب البيانات في مكون الخادم الخاص بك، ومرر الوعد (promise) إلى مكون العميل كخاصية (prop):

import Posts from '@/app/ui/posts
import { Suspense } from 'react'

export default function Page() {
  // لا تستخدم await مع دالة جلب البيانات
  const posts = getPosts()

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Posts posts={posts} />
    </Suspense>
  )
}
import Posts from '@/app/ui/posts
import { Suspense } from 'react'

export default function Page() {
  // لا تستخدم await مع دالة جلب البيانات
  const posts = getPosts()

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Posts posts={posts} />
    </Suspense>
  )
}

ثم، في مكون العميل الخاص بك، استخدم خطاف use لقراءة الوعد:

'use client'
import { use } from 'react'

export default function Posts({
  posts,
}: {
  posts: Promise<{ id: string; title: string }[]>
}) {
  const allPosts = use(posts)

  return (
    <ul>
      {allPosts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}
'use client'
import { use } from 'react'

export default function Posts({ posts }) {
  const posts = use(posts)

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

في المثال أعلاه، تم تغليف مكون <Posts> داخل حدود <Suspense>. هذا يعني أنه سيتم عرض الحالة الاحتياطية (fallback) أثناء حل الوعد. تعلم المزيد عن التدفق.

المكتبات المجتمعية

يمكنك استخدام مكتبة مجتمعية مثل SWR أو React Query لجلب البيانات في مكونات العميل. هذه المكتبات لها دلالاتها الخاصة للتخزين المؤقت والتدفق والميزات الأخرى. على سبيل المثال، مع SWR:

'use client'
import useSWR from 'swr'

const fetcher = (url) => fetch(url).then((r) => r.json())

export default function BlogPage() {
  const { data, error, isLoading } = useSWR(
    'https://api.vercel.app/blog',
    fetcher
  )

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>

  return (
    <ul>
      {data.map((post: { id: string; title: string }) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}
'use client'

import useSWR from 'swr'

const fetcher = (url) => fetch(url).then((r) => r.json())

export default function BlogPage() {
  const { data, error, isLoading } = useSWR(
    'https://api.vercel.app/blog',
    fetcher
  )

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>

  return (
    <ul>
      {data.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

إزالة تكرار الطلبات مع React.cache

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

إذا كنت تستخدم fetch، يمكن إزالة تكرار الطلبات بإضافة cache: 'force-cache'. هذا يعني أنه يمكنك استدعاء نفس الرابط بنفس الخيارات بأمان، وسيتم إجراء طلب واحد فقط.

إذا كنت لا تستخدم fetch، وتستخدم بدلاً من ذلك ORM أو قاعدة بيانات مباشرة، يمكنك تغليف جلب البيانات الخاصة بك باستخدام دالة React cache.

import { cache } from 'react'
import { db, posts, eq } from '@/lib/db'

export const getPost = cache(async (id: string) => {
  const post = await db.query.posts.findFirst({
    where: eq(posts.id, parseInt(id)),
  })
})
import { cache } from 'react'
import { db, posts, eq } from '@/lib/db'
import { notFound } from 'next/navigation'

export const getPost = cache(async (id) => {
  const post = await db.query.posts.findFirst({
    where: eq(posts.id, parseInt(id)),
  })
})

التدفق

تحذير: المحتوى أدناه يفترض تمكين خيار التكوين dynamicIO في تطبيقك. تم تقديم هذه العلامة في Next.js 15 canary.

عند استخدام async/await في مكونات الخادم، سيقوم Next.js بالانتقال إلى التقديم الديناميكي (dynamic rendering). هذا يعني أن البيانات سيتم جلبها وتقديمها على الخادم لكل طلب مستخدم. إذا كانت هناك أي طلبات بيانات بطيئة، فسيتم حظر المسار بالكامل من التقديم.

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

كيف يعمل التقديم من الخادم مع التدفق

هناك طريقتان يمكنك من خلالهما تنفيذ التدفق في تطبيقك:

  1. تغليف صفحة بملف loading.js
  2. تغليف مكون ب <Suspense>

مع loading.js

يمكنك إنشاء ملف loading.js في نفس مجلد صفحتك لتدفق الصفحة بأكملها أثناء جلب البيانات. على سبيل المثال، لتدفق app/blog/page.js، أضف الملف داخل مجلد app/blog.

هيكل مجلد المدونة مع ملف loading.js
export default function Loading() {
  // حدد واجهة التحميل هنا
  return <div>Loading...</div>
}
export default function Loading() {
  // حدد واجهة التحميل هنا
  return <div>Loading...</div>
}

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

واجهة التحميل

خلف الكواليس، سيتم تداخل loading.js داخل layout.js، وسيتم تغليف page.js وأي أطفال أدناه تلقائيًا داخل حدود <Suspense>.

نظرة عامة على loading.js

يعمل هذا النهج جيدًا لمقاطع المسار (التخطيطات والصفحات)، ولكن لمزيد من التدفق الدقيق، يمكنك استخدام <Suspense>.

مع <Suspense>

يسمح لك <Suspense> بأن تكون أكثر دقة بشأن الأجزاء التي تريد تدفقها من الصفحة. على سبيل المثال، يمكنك عرض أي محتوى صفحة يقع خارج حدود <Suspense> على الفور، وتدفق قائمة مشاركات المدونة داخل الحدود.

import { Suspense } from 'react'
import BlogList from '@/components/BlogList'
import BlogListSkeleton from '@/components/BlogListSkeleton'

export default function BlogPage() {
  return (
    <div>
      {/* سيتم إرسال هذا المحتوى إلى العميل على الفور */}
      <header>
        <h1>Welcome to the Blog</h1>
        <p>Read the latest posts below.</p>
      </header>
      <main>
        {/* أي محتوى مغلف داخل حدود <Suspense> سيتم تدفقه */}
        <Suspense fallback={<BlogListSkeleton />}>
          <BlogList />
        </Suspense>
      </main>
    </div>
  )
}
import { Suspense } from 'react'
import BlogList from '@/components/BlogList'
import BlogListSkeleton from '@/components/BlogListSkeleton'

export default function BlogPage() {
  return (
    <div>
      {/* سيتم إرسال هذا المحتوى إلى العميل على الفور */}
      <header>
        <h1>Welcome to the Blog</h1>
        <p>Read the latest posts below.</p>
      </header>
      <main>
        {/* أي محتوى مغلف داخل حدود <Suspense> سيتم تدفقه */}
        <Suspense fallback={<BlogListSkeleton />}>
          <BlogList />
        </Suspense>
      </main>
    </div>
  )
}

إنشاء حالات تحميل ذات معنى

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

في التطوير، يمكنك معاينة وفحص حالة التحميل لمكوناتك باستخدام React Devtools.

أمثلة

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

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

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

قد تكون هناك حالات تريد فيها هذا النمط لأن أحد عمليات الجلب يعتمد على نتيجة الآخر.

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

export default async function Page({
  params,
}: {
  params: Promise<{ username: string }>
}) {
  const { username } = await params
  // الحصول على معلومات الفنان
  const artist = await getArtist(username)

  return (
    <>
      <h1>{artist.name}</h1>
      {/* عرض واجهة احتياطية أثناء تحميل مكون Playlists */}
      <Suspense fallback={<div>Loading...</div>}>
        {/* تمرير معرف الفنان إلى مكون Playlists */}
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  )
}

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 }) {
  const { username } = await params
  // الحصول على معلومات الفنان
  const artist = await getArtist(username)

  return (
    <>
      <h1>{artist.name}</h1>
      {/* عرض واجهة احتياطية أثناء تحميل مكون Playlists */}
      <Suspense fallback={<div>Loading...</div>}>
        {/* تمرير معرف الفنان إلى مكون Playlists */}
        <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>
  )
}

لتحسين تجربة المستخدم، يجب عليك استخدام React <Suspense> لعرض fallback أثناء جلب البيانات. سيؤدي هذا إلى تمكين التدفق ومنع حظر المسار بالكامل بواسطة طلبات البيانات التسلسلية.

جلب البيانات المتوازي (Parallel data fetching)

يحدث جلب البيانات المتوازي عندما يتم بدء طلبات البيانات في المسار (route) بشكل متحمس (eagerly) وتنفيذها في نفس الوقت.

بشكل افتراضي، يتم عرض التخطيطات والصفحات بالتوازي. لذا يبدأ كل مقطع (segment) بجلب البيانات في أسرع وقت ممكن.

ومع ذلك، داخل أي مكون (component)، يمكن أن تظل طلبات async/await المتعددة متتابعة (sequential) إذا تم وضعها بعد بعضها البعض. على سبيل المثال، سيتم حظر getAlbums حتى يتم حل getArtist:

import { getArtist, getAlbums } from '@/app/lib/data'

export default async function Page({ params }) {
  // These requests will be sequential
  const { username } = await params
  const artist = await getArtist(username)
  const albums = await getAlbums(username)
  return <div>{artist.name}</div>
}

يمكنك بدء الطلبات بالتوازي عن طريق تعريفها خارج المكونات التي تستخدم البيانات، وحلها معًا، على سبيل المثال باستخدام Promise.all:

import Albums from './albums'

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

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

export default async function Page({
  params,
}: {
  params: Promise<{ username: string }>
}) {
  const { username } = await params
  const artistData = getArtist(username)
  const albumsData = getAlbums(username)

  // Initiate both requests in parallel
  const [artist, albums] = await Promise.all([artistData, albumsData])

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

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

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

export default async function Page({ params }) {
  const { username } = await params
  const artistData = getArtist(username)
  const albumsData = getAlbums(username)

  // Initiate both requests in parallel
  const [artist, albums] = await Promise.all([artistData, albumsData])

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

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

جلب البيانات المسبق (Preloading data)

يمكنك جلب البيانات مسبقًا عن طريق إنشاء دالة مساعدة (utility function) تقوم باستدعائها بشكل متحمس قبل الطلبات الحاجزة (blocking requests). يقوم <Item> بعرض مشروط بناءً على دالة checkIsAvailable().

يمكنك استدعاء preload() قبل checkIsAvailable() لبدء تبعيات بيانات <Item/> بشكل متحمس. بحلول وقت عرض <Item/>، تكون بياناته قد تم جلبها بالفعل.

import { getItem } from '@/lib/data'

export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  // starting loading item data
  preload(id)
  // perform another asynchronous task
  const isAvailable = await checkIsAvailable()

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

export const preload = (id: string) => {
  // void evaluates the given expression and returns undefined
  // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
  void getItem(id)
}
export async function Item({ id }: { id: string }) {
  const result = await getItem(id)
  // ...
}
import { getItem } from '@/lib/data'

export default async function Page({ params }) {
  const { id } = await params
  // starting loading item data
  preload(id)
  // perform another asynchronous task
  const isAvailable = await checkIsAvailable()

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

export const preload = (id) => {
  // void evaluates the given expression and returns undefined
  // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
  void getItem(id)
}
export async function Item({ id }) {
  const result = await getItem(id)
  // ...

بالإضافة إلى ذلك، يمكنك استخدام دالة cache من React وحزمة server-only لإنشاء دالة مساعدة قابلة لإعادة الاستخدام. تتيح لك هذه الطريقة تخزين دالة جلب البيانات مؤقتًا (cache) وضمان تنفيذها فقط على الخادم.

import { cache } from 'react'
import 'server-only'
import { getItem } from '@/lib/data'

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

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

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

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