---
title: TanStack Query + Next.js 16 App Router
description: 'اعمل Prefetch لـ TanStack Query v5 على السيرفر في Next.js 16 App Router، وبعدين اعمل hydrate باستخدام HydrationBoundary: من غير spinners، ومن غير double fetch. دليل عملي لسنة 2026.'
url: https://nerdleveltech.com/ar/tanstack-query-nextjs-app-router-prefetch-tutorial
language: ar
date: '2026-06-17'
category: frontend
tags:
  - tanstack query
  - react query
  - next.js
  - app router
  - hydration
  - ssr
  - frontend
  - tutorial
markdown_url: https://nerdleveltech.com/ar/tanstack-query-nextjs-app-router-prefetch-tutorial.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/ar/tanstack-query-nextjs-app-router-prefetch-tutorial'
terms: https://nerdleveltech.com/llms.txt
---

# TanStack Query + Next.js 16 App Router

لعمل prefetch لبيانات TanStack Query في App Router الخاص بـ Next.js، قم باستدعاء `prefetchQuery` على `QueryClient` مخصص لكل طلب داخل Server Component، ثم قم بتغليف الشجرة الفرعية للعميل في `<HydrationBoundary state={dehydrate(queryClient)}>`. سيقوم استدعاء `useQuery` مطابق على العميل بقراءة البيانات عند أول ظهور (first paint).

## ملخص

ستقوم بجلب قائمة على الخادم في App Router الخاص بـ Next.js 16 وتسليمها للعميل دون وميض تحميل (loading flash). يستخدم هذا البناء إصدارات `next` 16.2.9، و `React` 19.2.7، و `@tanstack/React-query` 5.101.0.[^npm] ستقوم بإنشاء مصنع (factory) واحد لـ `QueryClient` يكون عبارة عن عميل جديد لكل طلب على الخادم ونسخة وحيدة (singleton) في المتصفح، وتقوم بعمل prefetch في Server Component، وتسلسل ذاكرة التخزين المؤقت (cache) باستخدام `dehydrate`، وإعادة ترطيبها (rehydrate) من خلال `<HydrationBoundary>`. بعد ذلك، ستقوم بتعيين `staleTime` غير صفري حتى لا يقوم العميل بإعادة الجلب فوراً، وتنهي العمل بمتغير تدفقي (streaming variant) يستخدم `useSuspenseQuery`. خصص حوالي 25 دقيقة.

## ما ستتعلمه

- كيفية إنشاء مصنع `QueryClient` يكون لكل طلب على الخادم ونسخة وحيدة في المتصفح
- كيفية ربط `QueryClientProvider` في App Router باستخدام مكون Providers يحمل علامة `'use client'`
- كيفية عمل prefetch للبيانات في Server Component باستخدام `prefetchQuery`
- كيف يقوم كل من `dehydrate` و `HydrationBoundary` بنقل ذاكرة التخزين المؤقت من الخادم إلى العميل
- لماذا يستمر الاستعلام الذي تم عمل prefetch له في إعادة الجلب عند التحميل (mount)، وكيف يعالج `staleTime` ذلك
- كيفية دفق استعلام معلق باستخدام `useSuspenseQuery` و `shouldDehydrateQuery`
- كيفية عمل prefetch في مكان أقرب لمكان استخدام البيانات، بالإضافة إلى بديل العميل الواحد
- كيفية التحقق من نجاح عملية الترطيب (hydration)، وكيفية إصلاح أوضاع الفشل الشائعة

## المتطلبات الأساسية

- Node.js 20.9+ (يتطلبه Next.js 16؛ Node 24 LTS هو الإصدار الحالي المدعوم حتى أبريل 2028)[^node]
- تطبيق Next.js 16 App Router جديد: `npx create-next-app@16.2.9 my-app --ts --app`
- حزم محددة الإصدار، وهي الحالية على npm اعتباراً من 17 يونيو 2026:[^npm]

```bash
npm install --save-exact @tanstack/React-query@5.101.0 @tanstack/React-query-devtools@5.101.0
npm install --save-exact -D TypeScript@6.0.3 @types/React@19.2.17 @types/React-dom@19.2.3 @types/node@24.13.2
```

سوف نقوم بالجلب من `https://jsonplaceholder.typicode.com`، وهو API تجريبي عام مجاني لا يتطلب مفتاحاً، لذا فإن كل كتلة برمجية أدناه ستعمل كما هي مكتوبة.

أولاً، طبقة بيانات صغيرة. أنشئ `lib/posts.ts`:

```ts
export type Post = {
  id: number;
  title: string;
  body: string;
};

const API = "https://jsonplaceholder.typicode.com";

export async function getPosts(): Promise<Post[]> {
  const res = await fetch(`${API/posts?_limit=10`);
  if (!res.ok) throw new Error(`Failed to load posts: ${res.status`);
  return res.json() as Promise<Post[]>;
}
```

## الخطوة 1: إنشاء مصنع QueryClient يكون لكل طلب على الخادم

من الأخطاء الشائعة في App Router مشاركة `QueryClient` واحد عبر كل الطلبات على الخادم. الخادم طويل الأمد، لذا فإن العميل المشترك يسرب بيانات مستخدم واحد إلى ذاكرة التخزين المؤقت للمستخدم التالي. الحل هو مصنع يقوم دائماً ببناء عميل جديد على الخادم، ويعيد استخدام عميل واحد في المتصفح. أنشئ `app/get-query-client.ts`:

```ts
import {
  environmentManager,
  QueryClient,
  defaultShouldDehydrateQuery,
} from "@tanstack/React-query";

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        // Above 0 so the client treats prefetched data as fresh
        // and does NOT refetch on mount.
        staleTime: 60 * 1000,
      },
      dehydrate: {
        // Also ship queries that are still pending, so streaming works.
        shouldDehydrateQuery: (query) =>
          defaultShouldDehydrateQuery(query) ||
          query.state.status === "pending",
      },
    },
  });
}

let browserQueryClient: QueryClient | undefined = undefined;

export function getQueryClient() {
  if (environmentManager.isServer()) {
    // Server: a brand-new client for every request.
    return makeQueryClient();
  }
  // Browser: reuse one client for the whole tab.
  if (!browserQueryClient) browserQueryClient = makeQueryClient();
  return browserQueryClient;
}
```

هناك تفصيلان مهمان هنا. `environmentManager.isServer()` هي الطريقة التي تكتشف بها وثائق TanStack Query الحالية الخادم؛ في الإصدار 5.101.0 هي عبارة عن استدعاء دالة. الدروس القديمة تستورد قيمة منطقية بدلاً من ذلك (`import { isServer } from "@tanstack/React-query"` ثم `if (isServer)`) — هذا التصدير لا يزال يعمل ولكنه الآن مهجور لصالح `environmentManager`، لذا لا تدع قصاصة برمجية منسوخة تربكك.[^isserver] إن `staleTime` البالغ 60 ثانية وتجاوز `shouldDehydrateQuery` كلاهما سيظهر أهميتهما في خطوات لاحقة.

## الخطوة 2: ربط QueryClientProvider

يستخدم `QueryClientProvider` سياق (context) React، لذا يجب أن يتواجد في Client Component. أنشئ `app/providers.tsx`:

```tsx
"use client";

import { QueryClientProvider } from "@tanstack/React-query";
import { ReactQueryDevtools } from "@tanstack/React-query-devtools";
import { getQueryClient } from "./get-query-client";

export default function Providers({ children }: { children: React.ReactNode }) {
  // No useState: getQueryClient() already returns a stable per-tab client.
  const queryClient = getQueryClient();

  return (
    <QueryClientProvider client={queryClient>
      {children
      <ReactQueryDevtools initialIsOpen={false/>
    </QueryClientProvider>
  );
}
```

لاحظ التعليق: لا تقم بتغليف `getQueryClient()` في `useState`. تضمن النسخة الوحيدة في المتصفح بالفعل عميلاً مستقراً واحداً لكل علامة تبويب، ولا يضيف `useState` شيئاً هنا. ثم قم بتثبيت المزود (provider) مرة واحدة في `app/layout.tsx`:

```tsx
import Providers from "./providers";

export default function RootLayout({
  children
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <Providers>{children</Providers>
      </body>
    </html>
  );
}
```

## الخطوة 3: عمل prefetch لـ TanStack Query على الخادم في Server Component

الآن نأتي إلى جوهر الموضوع. يقوم Server Component بإنشاء عميل، وينتظر الـ prefetch، ويمرر ذاكرة التخزين المؤقت المتسلسلة إلى الأسفل من خلال `<HydrationBoundary>`.[^ssr] أنشئ `app/posts/page.tsx`:

```tsx
import { dehydrate, HydrationBoundary } from "@tanstack/React-query";
import { getQueryClient } from "../get-query-client";
import { getPosts } from "../../lib/posts";
import PostList from "./posts";

export default async function PostsPage() {
  const queryClient = getQueryClient();

  // Fetch on the server and warm the cache for this exact query key.
  await queryClient.prefetchQuery({
    queryKey: ["posts"],
    queryFn: getPosts,
  });

  return (
    <HydrationBoundary state={dehydrate(queryClient>
      <PostList />
    </HydrationBoundary>
  );
}
```

يقوم `prefetchQuery` بتشغيل `getPosts` على الخادم ويخزن النتيجة تحت المفتاح `["posts"]`. يقوم `dehydrate(queryClient)` بتسلسل ذاكرة التخزين المؤقت تلك إلى كائن بسيط، ويقوم `HydrationBoundary` — وهو نفسه Client Component — بإعادة بناء تلك المدخلات في عميل المتصفح قبل أن يتم تصيير (render) مكوناتك. العقد الأساسي هو مفتاح الاستعلام: يجب على العميل طلب *نفس* المفتاح بالضبط الذي تم عمل prefetch له.

## الخطوة 4: HydrationBoundary و dehydrate على العميل

يقرأ مكون العميل ذاكرة التخزين المؤقت باستخدام `useQuery` عادي. نظراً لأنه تم ترطيب المدخل، فإن البيانات تكون موجودة عند أول تصيير — لا توجد حالة تحميل تظهر فجأة. أنشئ `app/posts/posts.tsx`:

```tsx
"use client";

import { useQuery } from "@tanstack/React-query";
import { getPosts } from "../../lib/posts";

export default function PostList() {
  // Same queryKey as the server prefetch -> data is here on first paint.
  const { data, isPending isError error } = useQuery({
    queryKey: ["posts"],
    queryFn: getPosts
  });

  if (isPending) return <p>Loading posts...</p>;
  if (isError) return <p>Something went wrong: {error.message</p>;

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

لا تزال `queryFn` موجودة هنا، وهذا مقصود: فهي المرجع لعمليات التنقل من جانب العميل إلى هذا المسار حيث لم يتم تشغيل prefetch على الخادم، وهي مصدر إعادة الجلب بمجرد أن تصبح البيانات قديمة (stale). في أول تحميل تم عمل prefetch له، ببساطة لا يتم استدعاؤها.

## الخطوة 5: staleTime الذي يوقف الجلب المزدوج

هنا يكمن الفخ الخفي. القيمة الافتراضية لـ `staleTime` في TanStack Query هي `0`، مما يعني أن الاستعلام يعتبر قديماً في اللحظة التي يتم فيها تخزينه مؤقتاً. لذا حتى مع وجود prefetch مثالي، يتم تحميل العميل، ويرى مدخلاً "قديماً"، ويقوم فوراً بإعادة الجلب في الخلفية — وبذلك يضيع عمل الخادم وتظهر طلبات مكررة في علامة تبويب الشبكة (network tab). تعيين `staleTime` فوق الصفر في الخطوة 1 هو الحل: خلال أول 60 ثانية، تعتبر البيانات المرطبة "جديدة" (fresh)، لذا يقوم العميل بتصييرها ويتخطى إعادة الجلب.

يمكنك تأكيد الآلية مباشرة. مباشرة بعد prefetch مع `staleTime: 60 * 1000`، سيظهر الاستعلام المخزن مؤقتاً أن `isStale() === false`، وهذا هو بالضبط سبب عدم تشغيل إعادة الجلب عند التحميل. ارفع أو اخفض المدة لتناسب سرعة تغير بياناتك؛ فقط اجعلها فوق الصفر كلما قمت بعمل prefetch.

## الخطوة 6: دفق استعلام معلق باستخدام useSuspenseQuery

انتظار الـ prefetch يمنع الاستجابة حتى تصبح البيانات جاهزة. بالنسبة للبيانات الأبطأ، يمكنك بدلاً من ذلك *الدفق* (stream): ابدأ الجلب، وأرسل الاستعلام الذي لا يزال معلقاً، واتركه يكتمل عبر الشبكة داخل حدود Suspense.[^ssr] هذا هو السبب في أن `makeQueryClient` قام بتجاوز `shouldDehydrateQuery` ليشمل أيضاً الاستعلامات `pending` — حيث أن الوضع الافتراضي يتجاهلها. أنشئ `app/posts-streaming/page.tsx`:

```tsx
import { dehydrate HydrationBoundary } from "@tanstack/React-query";
import { Suspense } from "React";
import { getQueryClient } from "../get-query-client";
import { getPosts } from "../../lib/posts";
import StreamingPostList from "./posts";

// No `async`, no `await`: kick off the fetch and stream the result in.
export default function StreamingPostsPage() {
  const queryClient = getQueryClient();

  // Note: no await. The pending query is dehydrated and streamed.
  void queryClient.prefetchQuery({
    queryKey: ["posts"],
    queryFn: getPosts
  });

  return (
    <HydrationBoundary state={dehydrate(queryClient>
      <Suspense fallback={<p>Streaming posts...</p>>
        <StreamingPostList />
      </Suspense>
    </HydrationBoundary>
  );
}
```

يستخدم مكون العميل `useSuspenseQuery`، والذي لا يعيد أبداً بيانات `undefined` ويتوقف مؤقتاً (suspends) حتى يكتمل المدخل. أنشئ `app/posts-streaming/posts.tsx`:

```tsx
"use client";

import { useSuspenseQuery } from "@tanstack/React-query";
import { getPosts } from "../../lib/posts";

export default function StreamingPostList() {
  // Suspends until ready; data is always defined here.
  const { data } = useSuspenseQuery({
    queryKey: ["posts"],
    queryFn: getPosts,
  });

  return (
    <ul>
      {datamap((post) => (
        <li key={postid}>{posttitle}</li>
      ))}
    </ul>
  );
}
```

يعمل هذا لأن React يمكنه تحويل الـ promise المعلق (pending) إلى صيغة تسلسلية (serialize) للعميل، كما أن تجاوز `shouldDehydrateQuery` يضع الاستعلام المعلق في حالة الـ dehydrated منذ البداية.

## الجلب المسبق (Prefetch) بالقرب من مكان استخدام البيانات

لست مقيداً بالجلب المسبق في `page.tsx`. وبما أن مكونات الخادم (Server Components) متداخلة، يمكنك إجراء الجلب المسبق في `layout.tsx` أو في أي مكون خادم متداخل، مع وضع `<HydrationBoundary>` حول الشجرة الفرعية التي تحتاج البيانات فقط. وجود حدود (boundaries) متعددة في صفحة واحدة أمر جيد — فكل منها يمكنه إنشاء عميله الخاص وتجفيف (dehydrate) الجزء الخاص به. السبب وراء الجلب المسبق في مستويات أعمق هو التوازي: يقوم Next.js بجلب أجزاء المسار (route segments) بالتوازي، لذا فإن توزيع الجلب المسبق عبر التخطيط (layout)، والصفحة، والمسارات المتوازية يسمح للإطار بتسطيح ما كان سيصبح شلال طلبات (request waterfall) من جانب الخادم.[^ssr]

إذا كنت تفضل عميلاً واحداً للطلب بالكامل بدلاً من عميل لكل مكون خادم، فقم بلف مصنع واحد (single factory) في `cache` الخاص بـ React، والذي يكون نطاقه محدداً لكل طلب حتى لا يتسرب أي شيء بين المستخدمين:

```ts
// app/get-query-client.ts (بديل العميل الواحد لكل طلب)
import { QueryClient } from "@tanstack/React-query";
import { cache } from "React";

// cache() نطاقه محدد لكل طلب، لذا لا تتسرب البيانات أبداً بين الطلبات.
export const getQueryClient = cache(() => new QueryClient());
```

المقايضة هنا هي أن كل استدعاء لـ `dehydrate(getQueryClient())` يقوم بتحويل العميل *بأكمله* إلى صيغة تسلسلية، بما في ذلك الاستعلامات من مكونات الخادم غير المرتبطة، لذا ستدفع تكلفة إضافية بسيطة في التحويل التسلسلي مقابل راحة الحصول على نفس العميل في أي مكان.[^ssr]

## التحقق

قم بتشغيل `npm run dev` وافتح `/posts`. هناك ثلاثة فحوصات تؤكد نجاح عملية الـ hydration:

1. **عرض المصدر (View source).** عناوين `<li>` التي تم تصييرها موجودة في HTML الأولي، ولم يتم حقنها لاحقاً بواسطة JavaScript. إذا كانت القائمة مفقودة من HTML الخام، فهذا يعني أن الجلب المسبق لم يعمل.
2. **تبويب الشبكة (Network tab).** عند إعادة التحميل القوي لصفحة `/posts`، لا يوجد طلب من جانب العميل إلى API الخاص بالمنشورات في أول 60 ثانية. وجود طلب مكرر يعني أن `staleTime` لا يزال `0`.
3. **أدوات مطوري React Query.** يظهر استعلام `["posts"]` بالفعل في حالة `success` عند أول ظهور للرسم (first paint)، بدلاً من الانتقال من حالة `pending`.

يمكنك أيضاً التحقق من عقد الـ dehydration بشكل منفصل، بدون متصفح. احفظ هذا كـ `verify.mjs` وقم بتشغيل `node verify.mjs`:

```js
import { QueryClient, dehydrate } from "@tanstack/React-query";

const sample = [{ id: 1, title: "first", body: "..." }];

// Awaited prefetch -> يتم التقاط الاستعلام مع بياناته.
const qc = new QueryClient();
await qcprefetchQuery({ queryKey: ["posts"], queryFn: async () => sample });
consolelog("queries dehydrated:", dehydrate(qc)querieslength); // 1

// يتم إسقاط الاستعلام المعلق ما لم تختر تضمينه.
const qc2 = new QueryClient();
void qc2prefetchQuery({
  queryKey: ["posts"],
  queryFn: () => new Promise((r) => setTimeout(() => r(sample), 50)),
});
consolelog("default pending dehydrated:", dehydrate(qc2)querieslength); // 0
```

السجل الأول يطبع `1` والثاني يطبع `0`. هذا الـ `0` الثاني هو بالضبط السبب في أن البث (streaming) يحتاج إلى تجاوز `shouldDehydrateQuery` من الخطوة 1.

## الأخطاء الشائعة

**`dehydrate()` يعيد `{ queries: [] }` فارغاً.** إما أنك لم تقم بـ `await` للجلب المسبق (لذا كان الاستعلام لا يزال معلقاً عند التجفيف، والإعداد الافتراضي يسقط الاستعلامات المعلقة)، أو أنك قمت بتجفيف `QueryClient` *مختلف* عن الذي قمت بالجلب المسبق عليه. انتظر الجلب المسبق لنمط الحظر (blocking pattern)، أو أضف تجاوز `pending` وحدود Suspense لنمط البث.[^issue]

**البيانات صحيحة، لكن العميل يعيد الجلب فوراً عند التحميل.** هذا بسبب القيمة الافتراضية `staleTime: 0`. قم بتعيين `staleTime` غير صفري في `makeQueryClient` بحيث يتم التعامل مع البيانات التي تم عمل hydration لها على أنها طازجة.[^ssr]

**`environmentManager.isServer is not a function`.** أنت تستخدم إصداراً من TanStack Query أقدم من الإصدار الذي قدم `environmentManager` (تمت إضافته في pull request #10199). إما أن تقوم بالترقية إلى 5.101.0، أو تستخدم القيمة المنطقية (boolean) التي لا تزال مُصدرة بدلاً من ذلك: `import { isServer } from "@tanstack/React-query"` و `if (isServer)`.[^isserver]

**مفتاح الاستعلام (query key) غير متطابق.** تعتمد الـ hydration على مفتاح الاستعلام المتسلسل. `["posts"]` على الخادم و `["posts", undefined]` على العميل هما مفتاحان مختلفان، لذا سيعتبرهما العميل مفقودين ويقوم بالجلب مرة أخرى. حافظ على تطابق المفاتيح تماماً بايت مقابل بايت.

**حاولت الجلب باستخدام Server Action في `queryFn`.** تنصح TanStack Query بعدم القيام بذلك: تعمل Server Actions بشكل تسلسلي عند استدعائها من العميل، مما يتعارض مع كيفية قيام Query بالجلب وإعادة الجلب، ويمكن أن يترك الاستعلام عالقاً في حالة الانتظار (pending). قم بالجلب من route handler أو طبقة RPC بدلاً من ذلك، واحتفظ بـ Server Actions للعمليات التي تغير البيانات (mutations).[^serveraction]

## الخطوات التالية ومزيد من القراءة

لديك الآن بيانات TanStack Query تم جلبها مسبقاً من الخادم ومعمول لها hydration بدون وميض تحميل وبدون جلب مكرر. من هنا، اربط هذا مع [بث Next.js 16 و Suspense مع `use cache`](https://nerdleveltech.com/posts/nextjs-16-streaming-suspense-use-cache-tutorial) للتحكم في كيفية تصيير الهيكل (shell) المتدفق، واستخدم [Server Actions و واجهة المستخدم المتفائلة (optimistic UI) في Next.js 16](https://nerdleveltech.com/posts/nextjs-16-server-actions-React-19-optimistic-ui-tutorial) لجانب التعديلات الذي يقترن به `useMutation`. إذا كنت تستخدم أيضاً TanStack Router، فإن نفس الانضباط القائم على المفاتيح يظهر في [بارامترات البحث الآمنة من حيث النوع مع TanStack Router](https://nerdleveltech.com/posts/tanstack-router-type-safe-search-params-zod-tutorial).

[^npm]: تم تأكيد الإصدارات في سجل npm بتاريخ 17-06-2026: `next` 16.2.9، `React`/`React-dom` 19.2.7، `@tanstack/React-query` و `@tanstack/React-query-devtools` 5.101.0، `TypeScript` 6.0.3، `@types/React` 19.2.17، `@types/React-dom` 19.2.3، `@types/node` 24.13.2. <https://www.npmjs.com/package/@tanstack/React-query>

[^node]: جدول إصدارات Node.js (إصدار Node 24 "Krypton" Active LTS): <https://GitHub.com/nodejs/release> — ويصرح Next.js 16 بأن `engines.node` هو `>=20.9.0`.

[^isserver]: في `@tanstack/React-query` 5.101.0، يعد `environmentManager.isServer` دالة، بينما `isServer` الأقدم هو قيمة منطقية؛ كلاهما مُصدر، والقيمة المنطقية `isServer` أصبحت الآن مهجورة (deprecated) لصالح `environmentManager.isServer()` (TanStack/query PR #10199). تم التحقق من ذلك مقابل الحزمة المثبتة.

[^ssr]: "Advanced Server Rendering," وثائق TanStack Query v5 لـ React (أنماط الـ `get-query-client`، و `staleTime`، و `shouldDehydrateQuery`، والـ streaming): <https://tanstack.com/query/v5/docs/framework/React/guides/advanced-ssr>

[^issue]: تقارير متكررة في App Router تفيد بأن الاستعلام الذي تم جلبه مسبقاً (prefetched query) لا يزال يعيد الجلب على العميل عند التحميل (mount)، أو أن الـ `HydrationBoundary` يفشل في توصيل البيانات المجلوبة مسبقاً إلى المستهلك: مناقشة TanStack/query رقم 7184 ("useQuery fetches in client upon mount") والمشكلة رقم 8479 ("HydrationBoundary not working if prefetched query accessed via layout"). <https://GitHub.com/TanStack/query/discussions/7184>

[^serveraction]: دليل Advanced Server Rendering الخاص بـ TanStack Query يحذر من استخدام Next.js Server Actions للجلب داخل الـ `queryFn` (ويستشهد بالمشكلات TanStack/query رقم 7934 ورقم 6264): <https://tanstack.com/query/v5/docs/framework/React/guides/advanced-ssr>
