TanStack Query v5 مع Next.js App Router: الدليل الشامل لجلب البيانات

TanStack Query v5 هو المعيار الذهبي لإدارة حالة الخادم في React. عند دمجه مع Next.js App Router، يُنشئ طبقة بيانات قوية تتعامل مع التخزين المؤقت وإعادة الجلب في الخلفية والتحديثات التفاؤلية والمزيد — كل ذلك بأقل قدر من الشيفرة. في هذا الدرس، ستبني طبقة بيانات جاهزة للإنتاج من الصفر.
ما الذي ستبنيه
تطبيق BookShelf — مدير مجموعة كتب يتضمن:
- التحميل المسبق للاستعلامات من جانب الخادم مع Next.js App Router
- التخزين المؤقت وإعادة الجلب في الخلفية من جانب العميل
- التحديثات التفاؤلية لتحديث الواجهة فورياً
- التمرير اللانهائي مع ترقيم الصفحات
- الاستعلامات المعتمدة وجلب البيانات المتوازي
- حدود الأخطاء وحالات التحميل
- أدوات التطوير للتصحيح
المتطلبات الأساسية
قبل البدء، تأكد من توفر:
- Node.js 20+ مثبت
- معرفة أساسية بـ React و TypeScript
- إلمام بـ Next.js App Router (التخطيطات، الصفحات، مكونات الخادم)
- محرر أكواد (يُنصح بـ VS Code)
- فهم أساسي لواجهات REST APIs
الخطوة 1: إعداد المشروع
أنشئ مشروع Next.js جديد مع TypeScript:
npx create-next-app@latest bookshelf --typescript --tailwind --eslint --app --src-dir
cd bookshelfثبّت TanStack Query v5 والمكتبات المطلوبة:
npm install @tanstack/react-query @tanstack/react-query-devtools
npm install zodهيكل المشروع سيكون كالتالي:
bookshelf/
├── src/
│ ├── app/
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── books/
│ ├── lib/
│ │ ├── query-client.ts
│ │ └── api.ts
│ └── components/
│ └── providers.tsx
├── package.json
└── tsconfig.json
الخطوة 2: تكوين Query Client
Query Client هو نواة TanStack Query. يدير جميع ذاكرات التخزين المؤقت للاستعلامات والإعدادات الافتراضية وسلوك إعادة الجلب في الخلفية.
أنشئ ملف src/lib/query-client.ts:
import { QueryClient, defaultShouldDehydrateQuery, isServer } from "@tanstack/react-query";
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // دقيقة واحدة
gcTime: 5 * 60 * 1000, // 5 دقائق لجمع القمامة
retry: 2,
refetchOnWindowFocus: false,
},
dehydrate: {
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) ||
query.state.status === "pending",
},
},
});
}
let browserQueryClient: QueryClient | undefined = undefined;
export function getQueryClient() {
if (isServer) {
// الخادم: دائماً أنشئ query client جديد
return makeQueryClient();
} else {
// المتصفح: أنشئ query client جديد فقط إذا لم يكن موجوداً
if (!browserQueryClient) browserQueryClient = makeQueryClient();
return browserQueryClient;
}
}لماذا نفصل بين عملاء الخادم والمتصفح؟ على الخادم، يجب أن يحصل كل طلب على QueryClient جديد لمنع تسرب البيانات بين المستخدمين. على المتصفح، نعيد استخدام نفس العميل حتى تستمر الذاكرة المؤقتة عبر عمليات التنقل.
الخطوة 3: إنشاء مكون Providers
يحتاج TanStack Query إلى مزود يغلف تطبيقك. أنشئ ملف src/components/providers.tsx:
"use client";
import { QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { getQueryClient } from "@/lib/query-client";
export function Providers({ children }: { children: React.ReactNode }) {
const queryClient = getQueryClient();
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}حدّث ملف src/app/layout.tsx لاستخدام المزود:
import type { Metadata } from "next";
import { Providers } from "@/components/providers";
import "./globals.css";
export const metadata: Metadata = {
title: "BookShelf - TanStack Query Demo",
description: "مدير مجموعة كتب مبني بـ TanStack Query v5",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ar" dir="rtl">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}الخطوة 4: تعريف الأنواع وطبقة API
أنشئ تعريفات الأنواع ودوال API. أولاً، أنشئ ملف src/lib/types.ts:
import { z } from "zod";
export const BookSchema = z.object({
id: z.string(),
title: z.string(),
author: z.string(),
coverUrl: z.string().url(),
description: z.string(),
genre: z.string(),
rating: z.number().min(0).max(5),
publishedYear: z.number(),
createdAt: z.string().datetime(),
});
export type Book = z.infer<typeof BookSchema>;
export const BooksResponseSchema = z.object({
books: z.array(BookSchema),
nextCursor: z.string().nullable(),
totalCount: z.number(),
});
export type BooksResponse = z.infer<typeof BooksResponseSchema>;ثم أنشئ طبقة API في src/lib/api.ts:
import type { Book, BooksResponse } from "./types";
const API_BASE = "/api";
export async function fetchBooks(params: {
cursor?: string;
limit?: number;
genre?: string;
search?: string;
}): Promise<BooksResponse> {
const searchParams = new URLSearchParams();
if (params.cursor) searchParams.set("cursor", params.cursor);
if (params.limit) searchParams.set("limit", String(params.limit));
if (params.genre) searchParams.set("genre", params.genre);
if (params.search) searchParams.set("search", params.search);
const res = await fetch(`${API_BASE}/books?${searchParams}`);
if (!res.ok) throw new Error("فشل في جلب الكتب");
return res.json();
}
export async function fetchBook(id: string): Promise<Book> {
const res = await fetch(`${API_BASE}/books/${id}`);
if (!res.ok) throw new Error("الكتاب غير موجود");
return res.json();
}
export async function createBook(
data: Omit<Book, "id" | "createdAt">
): Promise<Book> {
const res = await fetch(`${API_BASE}/books`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error("فشل في إنشاء الكتاب");
return res.json();
}
export async function updateBook(
id: string,
data: Partial<Omit<Book, "id" | "createdAt">>
): Promise<Book> {
const res = await fetch(`${API_BASE}/books/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error("فشل في تحديث الكتاب");
return res.json();
}
export async function deleteBook(id: string): Promise<void> {
const res = await fetch(`${API_BASE}/books/${id}`, {
method: "DELETE",
});
if (!res.ok) throw new Error("فشل في حذف الكتاب");
}الخطوة 5: إنشاء مصانع مفاتيح الاستعلام
أحد أهم الأنماط في TanStack Query هو تنظيم مفاتيح الاستعلام. أنشئ ملف src/lib/queries.ts:
import { queryOptions, infiniteQueryOptions } from "@tanstack/react-query";
import { fetchBooks, fetchBook } from "./api";
export const bookKeys = {
all: ["books"] as const,
lists: () => [...bookKeys.all, "list"] as const,
list: (filters: { genre?: string; search?: string }) =>
[...bookKeys.lists(), filters] as const,
details: () => [...bookKeys.all, "detail"] as const,
detail: (id: string) => [...bookKeys.details(), id] as const,
};
// خيارات الاستعلام لكتاب واحد
export function bookQueryOptions(id: string) {
return queryOptions({
queryKey: bookKeys.detail(id),
queryFn: () => fetchBook(id),
staleTime: 5 * 60 * 1000, // 5 دقائق
});
}
// خيارات الاستعلام اللانهائي لقائمة الكتب
export function booksInfiniteQueryOptions(filters: {
genre?: string;
search?: string;
}) {
return infiniteQueryOptions({
queryKey: bookKeys.list(filters),
queryFn: ({ pageParam }) =>
fetchBooks({
cursor: pageParam,
limit: 12,
...filters,
}),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
}مصانع مفاتيح الاستعلام هي نمط يوصي به فريق TanStack. تُركّز جميع مفاتيح الاستعلام في مكان واحد، مما يجعل إبطال الذاكرة المؤقتة قابلاً للتنبؤ وآمناً من حيث الأنواع. الهيكل الهرمي يتيح لك الإبطال على أي مستوى — جميع الكتب، جميع القوائم، أو تفاصيل محددة.
الخطوة 6: التحميل المسبق من جانب الخادم
هنا يتألق دمج Next.js App Router مع TanStack Query. يمكنك تحميل البيانات مسبقاً على الخادم وتمريرها للعميل بدون تأخيرات متتالية.
أنشئ ملف src/app/books/page.tsx:
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
import { getQueryClient } from "@/lib/query-client";
import { booksInfiniteQueryOptions } from "@/lib/queries";
import { BookList } from "@/components/book-list";
interface BooksPageProps {
searchParams: Promise<{ genre?: string; search?: string }>;
}
export default async function BooksPage({ searchParams }: BooksPageProps) {
const params = await searchParams;
const queryClient = getQueryClient();
// التحميل المسبق على الخادم — البيانات متاحة فوراً على العميل
await queryClient.prefetchInfiniteQuery(
booksInfiniteQueryOptions({
genre: params.genre,
search: params.search,
})
);
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8">مكتبتي</h1>
<BookList
initialGenre={params.genre}
initialSearch={params.search}
/>
</div>
</HydrationBoundary>
);
}لصفحات الكتب الفردية، أنشئ ملف src/app/books/[id]/page.tsx:
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
import { getQueryClient } from "@/lib/query-client";
import { bookQueryOptions } from "@/lib/queries";
import { BookDetail } from "@/components/book-detail";
interface BookPageProps {
params: Promise<{ id: string }>;
}
export default async function BookPage({ params }: BookPageProps) {
const { id } = await params;
const queryClient = getQueryClient();
await queryClient.prefetchQuery(bookQueryOptions(id));
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<BookDetail id={id} />
</HydrationBoundary>
);
}الخطوة 7: بناء قائمة الكتب مع التمرير اللانهائي
أنشئ ملف src/components/book-list.tsx:
"use client";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useInView } from "react-intersection-observer";
import { useEffect, useState } from "react";
import { booksInfiniteQueryOptions } from "@/lib/queries";
import { BookCard } from "./book-card";
interface BookListProps {
initialGenre?: string;
initialSearch?: string;
}
export function BookList({ initialGenre, initialSearch }: BookListProps) {
const [genre, setGenre] = useState(initialGenre);
const [search, setSearch] = useState(initialSearch ?? "");
const { ref, inView } = useInView();
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
isError,
error,
} = useInfiniteQuery(booksInfiniteQueryOptions({ genre, search }));
// جلب الصفحة التالية تلقائياً عند ظهور عنصر المراقبة
useEffect(() => {
if (inView && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]);
const books = data?.pages.flatMap((page) => page.books) ?? [];
const totalCount = data?.pages[0]?.totalCount ?? 0;
if (isError) {
return (
<div className="text-red-500 p-4 rounded-lg bg-red-50">
خطأ في تحميل الكتب: {error.message}
</div>
);
}
return (
<div>
{/* شريط البحث والتصفية */}
<div className="flex gap-4 mb-6">
<input
type="text"
placeholder="ابحث عن كتب..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="flex-1 px-4 py-2 border rounded-lg"
/>
<select
value={genre ?? ""}
onChange={(e) => setGenre(e.target.value || undefined)}
className="px-4 py-2 border rounded-lg"
>
<option value="">جميع الأنواع</option>
<option value="fiction">رواية</option>
<option value="non-fiction">غير روائي</option>
<option value="sci-fi">خيال علمي</option>
<option value="biography">سيرة ذاتية</option>
</select>
</div>
{/* عدد النتائج */}
<p className="text-sm text-gray-500 mb-4">
عرض {books.length} من {totalCount} كتاب
</p>
{/* شبكة الكتب */}
{isLoading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="animate-pulse bg-gray-200 rounded-lg h-64" />
))}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{books.map((book) => (
<BookCard key={book.id} book={book} />
))}
</div>
)}
{/* عنصر مراقبة التمرير اللانهائي */}
<div ref={ref} className="h-10 mt-4">
{isFetchingNextPage && (
<p className="text-center text-gray-500">جاري تحميل المزيد...</p>
)}
</div>
</div>
);
}الخطوة 8: التحديثات التفاؤلية (Optimistic Mutations)
التحديثات التفاؤلية تجعل تطبيقك يبدو فورياً عن طريق تحديث الواجهة قبل استجابة الخادم. أنشئ ملف src/hooks/use-book-mutations.ts:
"use client";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { createBook, updateBook, deleteBook } from "@/lib/api";
import { bookKeys } from "@/lib/queries";
import type { Book } from "@/lib/types";
export function useCreateBook() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createBook,
onSuccess: () => {
// إبطال جميع قوائم الكتب لإعادة الجلب مع الكتاب الجديد
queryClient.invalidateQueries({ queryKey: bookKeys.lists() });
},
});
}
export function useUpdateBook(id: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: Partial<Omit<Book, "id" | "createdAt">>) =>
updateBook(id, data),
// التحديث التفاؤلي
onMutate: async (newData) => {
// إلغاء أي عمليات إعادة جلب جارية
await queryClient.cancelQueries({ queryKey: bookKeys.detail(id) });
// حفظ لقطة من القيمة السابقة
const previousBook = queryClient.getQueryData(bookKeys.detail(id));
// تحديث الذاكرة المؤقتة بشكل تفاؤلي
queryClient.setQueryData(bookKeys.detail(id), (old: Book | undefined) =>
old ? { ...old, ...newData } : old
);
return { previousBook };
},
// إذا فشل التعديل، ارجع للقيمة السابقة
onError: (_err, _newData, context) => {
if (context?.previousBook) {
queryClient.setQueryData(bookKeys.detail(id), context.previousBook);
}
},
// دائماً أعد الجلب بعد الخطأ أو النجاح
onSettled: () => {
queryClient.invalidateQueries({ queryKey: bookKeys.detail(id) });
queryClient.invalidateQueries({ queryKey: bookKeys.lists() });
},
});
}
export function useDeleteBook() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: deleteBook,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: bookKeys.lists() });
},
});
}نمط التحديث التفاؤلي يتبع ثلاث خطوات: (1) إلغاء الاستعلامات الصادرة، (2) حفظ لقطة من البيانات القديمة وتطبيق التحديث فوراً، (3) التراجع عند الخطأ. هذا يمنح المستخدمين استجابة فورية مع الحفاظ على تناسق البيانات.
الخطوة 9: الاستعلامات المعتمدة والمتوازية
أحياناً تحتاج لجلب بيانات تعتمد على بيانات أخرى. أنشئ ملف src/hooks/use-book-with-reviews.ts:
"use client";
import { useQuery, useQueries } from "@tanstack/react-query";
import { bookQueryOptions } from "@/lib/queries";
async function fetchReviews(bookId: string) {
const res = await fetch(`/api/books/${bookId}/reviews`);
if (!res.ok) throw new Error("فشل في جلب المراجعات");
return res.json();
}
async function fetchRelatedBooks(genre: string) {
const res = await fetch(`/api/books?genre=${genre}&limit=4`);
if (!res.ok) throw new Error("فشل في جلب الكتب المشابهة");
return res.json();
}
// استعلام معتمد: تُحمّل المراجعات بعد توفر بيانات الكتاب
export function useBookWithReviews(bookId: string) {
const bookQuery = useQuery(bookQueryOptions(bookId));
const reviewsQuery = useQuery({
queryKey: ["books", bookId, "reviews"],
queryFn: () => fetchReviews(bookId),
// جلب المراجعات فقط عند توفر بيانات الكتاب
enabled: !!bookQuery.data,
});
return { bookQuery, reviewsQuery };
}
// استعلامات متوازية: جلب موارد مستقلة متعددة في آن واحد
export function useBookPageData(bookId: string) {
const results = useQueries({
queries: [
bookQueryOptions(bookId),
{
queryKey: ["books", bookId, "reviews"],
queryFn: () => fetchReviews(bookId),
},
],
});
return {
book: results[0],
reviews: results[1],
};
}الخطوة 10: البحث مع تأخير الاستعلامات
لوظيفة البحث، تريد تأخير استدعاءات API. أنشئ ملف src/hooks/use-debounced-search.ts:
"use client";
import { useQuery } from "@tanstack/react-query";
import { useState, useDeferredValue } from "react";
import { fetchBooks } from "@/lib/api";
export function useDebouncedBookSearch() {
const [search, setSearch] = useState("");
const deferredSearch = useDeferredValue(search);
const query = useQuery({
queryKey: ["books", "search", deferredSearch],
queryFn: () => fetchBooks({ search: deferredSearch, limit: 10 }),
enabled: deferredSearch.length >= 2,
placeholderData: (previousData) => previousData, // إبقاء البيانات القديمة مرئية
});
return {
search,
setSearch,
isStale: search !== deferredSearch,
...query,
};
}يستخدم هذا useDeferredValue من React 19 بدلاً من مؤقت تأخير يدوي. خيار placeholderData يُبقي النتائج السابقة مرئية أثناء التحميل، مما يمنع انتقالات التخطيط.
الخطوة 11: معالجة الأخطاء واستراتيجيات إعادة المحاولة
أنشئ حدود أخطاء متينة. أنشئ ملف src/components/query-error-boundary.tsx:
"use client";
import { QueryErrorResetBoundary } from "@tanstack/react-query";
import { ErrorBoundary } from "react-error-boundary";
interface QueryErrorBoundaryProps {
children: React.ReactNode;
}
export function QueryErrorBoundary({ children }: QueryErrorBoundaryProps) {
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset}
fallbackRender={({ resetErrorBoundary, error }) => (
<div className="p-6 rounded-lg bg-red-50 border border-red-200">
<h3 className="text-lg font-semibold text-red-800 mb-2">
حدث خطأ ما
</h3>
<p className="text-red-600 mb-4">{error.message}</p>
<button
onClick={resetErrorBoundary}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
>
حاول مرة أخرى
</button>
</div>
)}
>
{children}
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
);
}يمكنك تخصيص سلوك إعادة المحاولة لكل استعلام:
useQuery({
queryKey: ["critical-data"],
queryFn: fetchCriticalData,
retry: (failureCount, error) => {
// لا تعد المحاولة عند أخطاء 404
if (error instanceof Error && error.message.includes("not found")) {
return false;
}
// أعد المحاولة حتى 3 مرات للأخطاء الأخرى
return failureCount < 3;
},
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
});الخطوة 12: أنماط إبطال الاستعلامات
فهم متى وكيف تُبطل الاستعلامات أمر حاسم. إليك الأنماط الرئيسية:
const queryClient = useQueryClient();
// 1. إبطال كل شيء
queryClient.invalidateQueries();
// 2. إبطال جميع الكتب (القوائم + التفاصيل)
queryClient.invalidateQueries({ queryKey: bookKeys.all });
// 3. إبطال قوائم الكتب فقط
queryClient.invalidateQueries({ queryKey: bookKeys.lists() });
// 4. إبطال قائمة محددة بفلاتر
queryClient.invalidateQueries({
queryKey: bookKeys.list({ genre: "fiction" }),
});
// 5. إبطال تفاصيل كتاب واحد
queryClient.invalidateQueries({ queryKey: bookKeys.detail("book-123") });
// 6. إزالة استعلام من الذاكرة المؤقتة بالكامل
queryClient.removeQueries({ queryKey: bookKeys.detail("deleted-book") });
// 7. تحميل مسبق لبيانات تنقل محتمل
queryClient.prefetchQuery(bookQueryOptions("likely-next-book-id"));نصيحة احترافية: استخدم invalidateQueries في معظم الحالات — فهي تُعلّم البيانات كقديمة وتُعيد الجلب في الخلفية. استخدم removeQueries فقط عندما لا يجب أن تكون البيانات موجودة بعد الآن (مثل بعد حذف مورد). استخدم setQueryData للتحديثات التفاؤلية حيث تعرف القيمة الجديدة.
الخطوة 13: معالجات مسارات API
أنشئ مسارات API لتشغيل تطبيقك. أنشئ ملف src/app/api/books/route.ts:
import { NextRequest, NextResponse } from "next/server";
// مخزن في الذاكرة لأغراض العرض التوضيحي
const books = new Map<string, any>();
// بذر بعض البيانات
const genres = ["fiction", "non-fiction", "sci-fi", "biography"];
for (let i = 1; i <= 50; i++) {
const id = `book-${i}`;
books.set(id, {
id,
title: `عنوان الكتاب ${i}`,
author: `المؤلف ${i}`,
coverUrl: `https://picsum.photos/seed/${id}/300/400`,
description: `كتاب رائع عن الموضوع ${i}.`,
genre: genres[i % genres.length],
rating: Math.round((Math.random() * 4 + 1) * 10) / 10,
publishedYear: 2020 + (i % 7),
createdAt: new Date().toISOString(),
});
}
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const cursor = searchParams.get("cursor");
const limit = parseInt(searchParams.get("limit") ?? "12");
const genre = searchParams.get("genre");
const search = searchParams.get("search");
let allBooks = Array.from(books.values());
if (genre) {
allBooks = allBooks.filter((b) => b.genre === genre);
}
if (search) {
const lowerSearch = search.toLowerCase();
allBooks = allBooks.filter(
(b) =>
b.title.toLowerCase().includes(lowerSearch) ||
b.author.toLowerCase().includes(lowerSearch)
);
}
const startIndex = cursor
? allBooks.findIndex((b) => b.id === cursor) + 1
: 0;
const paginated = allBooks.slice(startIndex, startIndex + limit);
const nextCursor =
startIndex + limit < allBooks.length
? allBooks[startIndex + limit - 1]?.id
: null;
return NextResponse.json({
books: paginated,
nextCursor,
totalCount: allBooks.length,
});
}
export async function POST(request: NextRequest) {
const data = await request.json();
const id = `book-${Date.now()}`;
const book = {
...data,
id,
createdAt: new Date().toISOString(),
};
books.set(id, book);
return NextResponse.json(book, { status: 201 });
}اختبار التطبيق
شغّل خادم التطوير:
npm run devافتح أدوات تطوير TanStack Query (الأيقونة العائمة في الزاوية السفلى اليمنى) لفحص:
- الاستعلامات النشطة — عرض جميع الاستعلامات الحالية وحالتها
- إدخالات الذاكرة المؤقتة — فحص البيانات المخزنة مؤقتاً
- الجدول الزمني للاستعلامات — مراقبة أنماط الجلب وإعادة الجلب
- مؤشرات القِدَم/الحداثة — فهم إعدادات staleTime
اختبر السيناريوهات التالية:
- التنقل بين الصفحات — لاحظ كيف يتم تخزين البيانات مؤقتاً وتكون متاحة فوراً عند العودة
- التصفية حسب النوع — شاهد الاستعلامات الجديدة تُطلق بينما تبقى النتائج المخزنة متاحة
- تعديل كتاب — شاهد التحديث التفاؤلي يحدث فوراً ثم يتأكد من الخادم
- قطع الاتصال — تبقى البيانات المخزنة متاحة وتنتظر التعديلات حتى استعادة الاتصال
استكشاف الأخطاء وإصلاحها
المشاكل الشائعة وحلولها:
أخطاء عدم تطابق الترطيب (Hydration)
تأكد أن getQueryClient() تُرجع نفس العميل على المتصفح لكن عميلاً جديداً على الخادم. النمط في الخطوة 2 يعالج هذا بشكل صحيح.
إعادة جلب الاستعلامات بشكل مفرط
زِد قيمة staleTime في خيارات الاستعلام. القيمة 0 (الافتراضية) تعني أن البيانات قديمة فوراً وستُعاد جلبها عند كل تحميل لمكون.
التمرير اللانهائي لا يعمل
تأكد أن getNextPageParam تُرجع undefined (وليس null) عند عدم وجود المزيد من الصفحات. TanStack Query يتحقق من undefined تحديداً.
البيانات لا تتحدث بعد التعديل
تأكد أن استدعاء invalidateQueries يستخدم نطاق مفتاح الاستعلام الصحيح. استخدم نمط مصنع مفاتيح الاستعلام للحفاظ على تناسق المفاتيح.
الخطوات التالية
الآن بعد أن أصبح لديك أساس متين مع TanStack Query v5 و Next.js:
- أضف دعم Suspense — استخدم
useSuspenseQueryلنمط تحميل أكثر تصريحية - نفّذ دعم العمل بدون اتصال — استخدم
@tanstack/query-persist-client-coreلحفظ الذاكرة المؤقتة في localStorage - أضف اشتراكات WebSocket — ادمج TanStack Query مع مصادر البيانات الحية
- استكشف إلغاء الاستعلامات — استخدم
AbortSignalلإلغاء الطلبات الجارية - ابنِ طابور تعديلات — تعامل مع التعديلات بدون اتصال التي تتزامن عند استعادة الاتصال
الخلاصة
يُحوّل TanStack Query v5 طريقة تعاملك مع حالة الخادم في تطبيقات Next.js. من خلال دمج التحميل المسبق من جانب الخادم مع التخزين المؤقت من جانب العميل، تحصل على أفضل ما في العالمين: تحميل أولي سريع وتنقلات لاحقة فورية. نمط مصنع مفاتيح الاستعلام يجعل إبطال الذاكرة المؤقتة قابلاً للتنبؤ، بينما التحديثات التفاؤلية تجعل واجهتك تبدو متجاوبة.
النقاط الرئيسية:
- افصل عملاء الخادم والمتصفح لمنع تسرب البيانات
- استخدم HydrationBoundary لنقل البيانات المحمّلة مسبقاً من الخادم للعميل
- نظّم مفاتيح الاستعلام بدوال مصنع لإبطال قابل للتنبؤ
- نفّذ التحديثات التفاؤلية للتعديلات التي تحتاج استجابة فورية
- استفد من الاستعلامات اللانهائية للقوائم المرقمة مع التحميل التلقائي
TanStack Query ليس مجرد مكتبة لجلب البيانات — إنه حل شامل لإدارة حالة الخادم يُقلل الشيفرة المكررة ويمنع الأخطاء ويجعل تطبيقك أسرع بشكل ملحوظ.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

إضافة المصادقة لتطبيق Next.js 15 باستخدام Auth.js v5: البريد الإلكتروني وOAuth والتحكم بالأدوار
تعلم كيفية إضافة نظام مصادقة جاهز للإنتاج لتطبيق Next.js 15 باستخدام Auth.js v5. يغطي هذا الدليل الشامل تسجيل الدخول عبر Google OAuth وبيانات الاعتماد بالبريد الإلكتروني وكلمة المرور والمسارات المحمية والتحكم بالوصول حسب الأدوار.

بناء روبوت دردشة ذكاء اصطناعي محلي باستخدام Ollama و Next.js: الدليل الشامل
ابنِ روبوت دردشة ذكاء اصطناعي خاص يعمل بالكامل على جهازك المحلي باستخدام Ollama و Next.js. يغطي هذا الدليل العملي التثبيت والبث المباشر واختيار النماذج وبناء واجهة دردشة جاهزة للإنتاج — كل ذلك دون إرسال بياناتك إلى السحابة.

بناء موقع محتوى متكامل باستخدام Payload CMS 3 و Next.js
تعلّم كيفية بناء موقع محتوى كامل الميزات باستخدام Payload CMS 3 الذي يعمل مباشرة داخل Next.js App Router. يغطي هذا الدرس المجموعات، محرر النصوص الغنية، رفع الوسائط، المصادقة، والنشر في بيئة الإنتاج.