⏱️4 min read · 785 words
يعد TanStack Query (المعروف سابقًا باسم React Query) هو المعيار الذهبي لإدارة حالة الخادم في تطبيقات React وVue وSolid وAngular في عام 2026. فهو يزيل تعقيد جلب البيانات يدويًا والتخزين المؤقت والمزامنة، ويستبدل مئات الأسطر من كود useEffect بخطافات بسيطة وقوية.
📋 Table of Contents
لماذا استعلام TanStack؟
- لا يوجد طبق معياري– يستبدل أنماط Redux + fetch + useEffect
- التخزين المؤقت التلقائي— البيانات المخزنة مؤقتًا بواسطة المفتاح، ويتم تحديثها عند الحاجة
- إعادة جلب الخلفية– تظهر البيانات القديمة على الفور، والبيانات الجديدة في الخلفية
- حالات التحميل/الخطأ— مدمج، ولا توجد إدارة يدوية للحالة
- تحديثات متفائلة– تحديثات واجهة المستخدم قبل تأكيد الخادم
- أدوات التطوير– فحص ذاكرة التخزين المؤقت، إعادة الجلب، إبطال امتداد المتصفح
التثبيت والإعداد
npm install @tanstack/react-query @tanstack/react-query-devtools
# Optional: axios for HTTP
npm install axios
// main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes (data considered fresh)
gcTime: 10 * 60 * 1000, // 10 minutes (cache garbage collection)
retry: 2, // retry failed requests twice
refetchOnWindowFocus: true, // refetch when tab becomes active
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<Router />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
useQuery – جلب البيانات
import { useQuery, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';
interface User {
id: number;
name: string;
email: string;
}
// Query function
async function fetchUser(id: number): Promise<User> {
const { data } = await axios.get(`/api/users/${id}`);
return data;
}
// Component using query
function UserProfile({ userId }: { userId: number }) {
const {
data: user,
isLoading,
isError,
error,
isFetching, // true when refetching in background
isStale, // true when data is stale
refetch, // manually trigger refetch
} = useQuery({
queryKey: ['users', userId], // unique cache key
queryFn: () => fetchUser(userId),
enabled: userId > 0, // only fetch when userId is valid
staleTime: 60_000, // fresh for 1 minute
select: (data) => data, // transform data before returning
});
if (isLoading) return <Skeleton />;
if (isError) return <ErrorBoundary error={error} />;
return (
<div>
{isFetching && <small>Refreshing...</small>}
<h1>{user.name}</h1>
<p>{user.email}</p>
<button onClick={() => refetch()}>Refresh</button>
</div>
);
}
الاستعلامات الموازية
// Fetch multiple queries simultaneously
function Dashboard() {
const [userQuery, postsQuery, statsQuery] = useQueries({
queries: [
{ queryKey: ['user', 1], queryFn: () => fetchUser(1) },
{ queryKey: ['posts'], queryFn: fetchPosts },
{ queryKey: ['stats'], queryFn: fetchStats },
],
});
if (userQuery.isLoading || postsQuery.isLoading) return <Loading />;
return (
<div>
<UserCard user={userQuery.data} />
<PostList posts={postsQuery.data} />
<StatsPanel stats={statsQuery.data} />
</div>
);
}
useMutation — الإنشاء والتحديث والحذف
import { useMutation, useQueryClient } from '@tanstack/react-query';
async function createPost(post: { title: string; content: string }) {
const { data } = await axios.post('/api/posts', post);
return data;
}
function NewPostForm() {
const queryClient = useQueryClient();
const { mutate, isPending, isError, error } = useMutation({
mutationFn: createPost,
// Called when mutation succeeds
onSuccess: (newPost) => {
// Option 1: Invalidate cache (triggers refetch)
queryClient.invalidateQueries({ queryKey: ['posts'] });
// Option 2: Update cache directly (no refetch)
queryClient.setQueryData(['posts'], (old: Post[]) => [...old, newPost]);
// Show toast notification
toast.success('Post created!');
},
onError: (error) => {
toast.error(`Failed: ${error.message}`);
},
});
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const form = e.currentTarget;
mutate({
title: (form.title as HTMLInputElement).value,
content: (form.content as HTMLTextAreaElement).value,
});
};
return (
<form onSubmit={handleSubmit}>
<input name="title" required placeholder="Title" />
<textarea name="content" required placeholder="Content" />
<button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create Post'}
</button>
{isError && <p className="error">{error.message}</p>}
</form>
);
}
تحديثات متفائلة
const { mutate: likePost } = useMutation({
mutationFn: (postId: number) => axios.post(`/api/posts/${postId}/like`),
onMutate: async (postId) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({ queryKey: ['posts', postId] });
// Snapshot the previous value
const previousPost = queryClient.getQueryData(['posts', postId]);
// Optimistically update the UI
queryClient.setQueryData(['posts', postId], (old: Post) => ({
...old,
likes: old.likes + 1,
}));
// Return context for rollback
return { previousPost };
},
// Roll back on error
onError: (error, postId, context) => {
queryClient.setQueryData(['posts', postId], context?.previousPost);
toast.error('Failed to like post');
},
// Always refetch after mutation
onSettled: (data, error, postId) => {
queryClient.invalidateQueries({ queryKey: ['posts', postId] });
},
});
استعلامات لا نهاية لها
import { useInfiniteQuery } from '@tanstack/react-query';
function InfinitePostList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['posts', 'infinite'],
queryFn: ({ pageParam = 1 }) => fetchPosts(pageParam),
initialPageParam: 1,
getNextPageParam: (lastPage, pages) =>
lastPage.hasMore ? pages.length + 1 : undefined,
});
return (
<div>
{data?.pages.map((page, i) => (
<React.Fragment key={i}>
{page.posts.map(post => <PostCard key={post.id} post={post} />)}
</React.Fragment>
))}
<button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage}>
{isFetchingNextPage ? 'Loading...' : hasNextPage ? 'Load More' : 'All loaded'}
</button>
</div>
);
}
خطافات الاستعلام المخصصة
// hooks/useUsers.ts — reusable query hooks
export function useUsers(filters?: UserFilters) {
return useQuery({
queryKey: ['users', filters],
queryFn: () => fetchUsers(filters),
select: (data) => ({
users: data.users,
total: data.total,
}),
});
}
export function useUser(id: number) {
const queryClient = useQueryClient();
return useQuery({
queryKey: ['users', id],
queryFn: () => fetchUser(id),
// Pre-populate from list query
initialData: () => {
const usersQuery = queryClient.getQueryData<UsersResponse>(['users']);
return usersQuery?.users.find(u => u.id === id);
},
initialDataUpdatedAt: () =>
queryClient.getQueryState(['users'])?.dataUpdatedAt,
});
}
export function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (user: Partial<User> & { id: number }) =>
axios.patch(`/api/users/${user.id}`, user).then(r => r.data),
onSuccess: (updated) => {
queryClient.setQueryData(['users', updated.id], updated);
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
}
يزيل استعلام TanStack في عام 2026 80% من النموذج النموذجي في كود جلب بيانات React النموذجي. ابدأ باستخدام useQuery للقراءة، وuseMutation للكتابة. استخدم validateQueries لتحديثات ذاكرة التخزين المؤقت البسيطة وsetQueryData للحصول على تحديثات متفائلة. الاستثمار في التعلم يؤتي ثماره في كل مشروع.
🔗 Share this article
✍️ Leave a Comment