7.8 שליפת נתונים React Query הרצאה
שליפת נתונים - React Query (TanStack Query)¶
בשיעור הזה נלמד על TanStack Query (לשעבר React Query) - הספרייה המובילה לניהול שליפת נתונים מהשרת באפליקציות ריאקט.
הבעיה עם useEffect ו-fetch¶
// הגישה הנאיבית - הרבה בעיות
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch("/api/users")
.then((res) => res.json())
.then((data) => {
if (!cancelled) setUsers(data);
})
.catch((err) => {
if (!cancelled) setError(err.message);
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => { cancelled = true; };
}, []);
// ...
}
בעיות בגישה הזו:
- צריך לנהל loading, error ו-data ידנית בכל מקום
- אין מטמון (cache) - כל מעבר לדף טוען מחדש
- אין ניהול של מצבי רשת (offline, reconnect)
- אין refetch אוטומטי
- טיפול ב-race conditions (cancelled) מסורבל
- שכפול קוד בכל קומפוננטה שטוענת נתונים
התקנה¶
הגדרת ה-Provider:
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 דקות
retry: 3,
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<MyApp />
</QueryClientProvider>
);
}
שליפת נתונים - useQuery¶
שימוש בסיסי¶
import { useQuery } from "@tanstack/react-query";
interface User {
id: number;
name: string;
email: string;
}
function UserList() {
const { data, isLoading, isError, error, refetch } = useQuery<User[]>({
queryKey: ["users"],
queryFn: async () => {
const response = await fetch("/api/users");
if (!response.ok) throw new Error("Failed to fetch users");
return response.json();
},
});
if (isLoading) return <p>טוען...</p>;
if (isError) return <p>שגיאה: {error.message}</p>;
return (
<div>
<button onClick={() => refetch()}>רענן</button>
<ul>
{data?.map((user) => (
<li key={user.id}>{user.name} - {user.email}</li>
))}
</ul>
</div>
);
}
queryKey- מזהה ייחודי לנתונים (משמש גם למטמון)queryFn- הפונקציה שמביאה את הנתונים- מוחזרים: data, isLoading, isError, error, refetch ועוד
מפתחות שאילתה - Query Keys¶
// מפתח פשוט
useQuery({ queryKey: ["users"], queryFn: fetchUsers });
// מפתח עם פרמטרים
useQuery({ queryKey: ["users", userId], queryFn: () => fetchUser(userId) });
// מפתח עם פילטרים
useQuery({
queryKey: ["users", { role: "admin", page: 1 }],
queryFn: () => fetchUsers({ role: "admin", page: 1 }),
});
// מפתח מקונן
useQuery({
queryKey: ["users", userId, "posts"],
queryFn: () => fetchUserPosts(userId),
});
- המפתח חייב להיות ייחודי לנתונים
- שינוי במפתח גורם לשליפה מחדש
- המפתח הוא מערך - כל שינוי באחד האלמנטים מפעיל refetch
אפשרויות נפוצות¶
useQuery({
queryKey: ["users"],
queryFn: fetchUsers,
staleTime: 5 * 60 * 1000, // נתונים "טריים" למשך 5 דקות
gcTime: 30 * 60 * 1000, // נתונים נשמרים במטמון 30 דקות
refetchOnWindowFocus: true, // רענון כשחוזרים לחלון
refetchInterval: 60000, // רענון כל דקה
enabled: !!userId, // לא לשלוף עד שיש userId
retry: 3, // נסיונות חוזרים בכישלון
select: (data) => data.filter((u) => u.active), // טרנספורמציה
});
מוטציות - useMutation¶
useMutation משמש לפעולות שמשנות נתונים בשרת (POST, PUT, DELETE):
import { useMutation, useQueryClient } from "@tanstack/react-query";
function CreateUserForm() {
const queryClient = useQueryClient();
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const createUser = useMutation({
mutationFn: async (newUser: { name: string; email: string }) => {
const response = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newUser),
});
if (!response.ok) throw new Error("Failed to create user");
return response.json();
},
onSuccess: () => {
// ביטול תוקף המטמון - יגרום לשליפה מחדש
queryClient.invalidateQueries({ queryKey: ["users"] });
setName("");
setEmail("");
},
onError: (error) => {
alert(`שגיאה: ${error.message}`);
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
createUser.mutate({ name, email });
};
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={(e) => setName(e.target.value)} placeholder="שם" />
<input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="אימייל" />
<button type="submit" disabled={createUser.isPending}>
{createUser.isPending ? "יוצר..." : "צור משתמש"}
</button>
</form>
);
}
עדכון ומחיקה¶
function UserItem({ user }: { user: User }) {
const queryClient = useQueryClient();
const updateUser = useMutation({
mutationFn: async (updates: Partial<User>) => {
const response = await fetch(`/api/users/${user.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(updates),
});
return response.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["users"] });
},
});
const deleteUser = useMutation({
mutationFn: async () => {
await fetch(`/api/users/${user.id}`, { method: "DELETE" });
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["users"] });
},
});
return (
<div>
<span>{user.name}</span>
<button onClick={() => updateUser.mutate({ name: "שם חדש" })}>
ערוך
</button>
<button onClick={() => deleteUser.mutate()}>מחק</button>
</div>
);
}
ביטול תוקף ושליפה מחדש - Invalidation¶
const queryClient = useQueryClient();
// ביטול תוקף של שאילתה ספציפית
queryClient.invalidateQueries({ queryKey: ["users"] });
// ביטול תוקף של כל השאילתות שמתחילות ב-"users"
queryClient.invalidateQueries({ queryKey: ["users"], exact: false });
// ביטול תוקף של הכל
queryClient.invalidateQueries();
// עדכון ישיר של המטמון
queryClient.setQueryData(["users"], (oldData: User[]) => [
...oldData,
newUser,
]);
עדכונים אופטימיסטיים - Optimistic Updates¶
עדכון ה-UI לפני שהשרת מאשר - מספק תחושת מהירות:
const toggleTodo = useMutation({
mutationFn: async (todoId: number) => {
const response = await fetch(`/api/todos/${todoId}/toggle`, {
method: "PATCH",
});
return response.json();
},
onMutate: async (todoId) => {
// ביטול שאילתות פעילות כדי שלא ידרסו את העדכון שלנו
await queryClient.cancelQueries({ queryKey: ["todos"] });
// שמירת ה-state הקודם
const previousTodos = queryClient.getQueryData(["todos"]);
// עדכון אופטימיסטי
queryClient.setQueryData(["todos"], (old: Todo[]) =>
old.map((t) => (t.id === todoId ? { ...t, done: !t.done } : t))
);
return { previousTodos };
},
onError: (_err, _todoId, context) => {
// שחזור ה-state הקודם בשגיאה
queryClient.setQueryData(["todos"], context?.previousTodos);
},
onSettled: () => {
// רענון מהשרת בכל מקרה
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});
דפדוף - Pagination¶
function PaginatedUsers() {
const [page, setPage] = useState(1);
const { data, isLoading, isPreviousData } = useQuery({
queryKey: ["users", page],
queryFn: () => fetchUsers(page),
placeholderData: (previousData) => previousData, // שמירת נתונים קודמים בזמן טעינה
});
return (
<div>
{isLoading ? (
<p>טוען...</p>
) : (
<ul style={{ opacity: isPreviousData ? 0.5 : 1 }}>
{data?.users.map((user: User) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)}
<div>
<button onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={page === 1}>
הקודם
</button>
<span>עמוד {page}</span>
<button
onClick={() => setPage((p) => p + 1)}
disabled={!data?.hasMore}
>
הבא
</button>
</div>
</div>
);
}
גלילה אינסופית - useInfiniteQuery¶
import { useInfiniteQuery } from "@tanstack/react-query";
function InfiniteUserList() {
const {
data,
isLoading,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ["users", "infinite"],
queryFn: async ({ pageParam }) => {
const response = await fetch(`/api/users?page=${pageParam}&limit=20`);
return response.json();
},
initialPageParam: 1,
getNextPageParam: (lastPage, allPages) => {
return lastPage.hasMore ? allPages.length + 1 : undefined;
},
});
if (isLoading) return <p>טוען...</p>;
const allUsers = data?.pages.flatMap((page) => page.users) ?? [];
return (
<div>
<ul>
{allUsers.map((user: User) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? "טוען..." : "טען עוד"}
</button>
)}
</div>
);
}
getNextPageParamמחזיר את הפרמטר לעמוד הבא, או undefined אם אין עודdata.pagesהוא מערך של כל העמודים שנטענוfetchNextPageטוען את העמוד הבא
אסטרטגיות מטמון - Caching Strategies¶
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// נתונים "טריים" למשך 5 דקות - לא ישלפו מחדש
staleTime: 5 * 60 * 1000,
// נתונים נשמרים במטמון 30 דקות אחרי שאין צורכים
gcTime: 30 * 60 * 1000,
// רענון כשחוזרים לחלון
refetchOnWindowFocus: true,
// לא לרענן כש-component עולה מחדש אם הנתונים טריים
refetchOnMount: "always",
},
},
});
- staleTime: כמה זמן נתונים נחשבים "טריים" (לא צריך refetch)
- gcTime: כמה זמן לשמור נתונים במטמון אחרי שאין component שמשתמש בהם
- כשנתונים stale, הם עדיין מוצגים מהמטמון בזמן ש-refetch רץ ברקע
DevTools¶
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
function App() {
return (
<QueryClientProvider client={queryClient}>
<MyApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
סיכום¶
- TanStack Query מנהל שליפת נתונים מהשרת עם מטמון, retry, ו-background refetch
- useQuery לשליפת נתונים (GET), useMutation לשינויים (POST/PUT/DELETE)
- Query Keys מזהים נתונים ייחודית ומפעילים refetch כשמשתנים
- invalidateQueries מאפשר לרענן נתונים אחרי mutation
- עדכונים אופטימיסטיים מספקים UX מהיר עם אפשרות חזרה בשגיאה
- תמיכה מובנית בדפדוף וגלילה אינסופית
- מטמון חכם עם staleTime ו-gcTime
- DevTools לדיבאג של שאילתות ומטמון