RTK Query
מבוא
RTK Query הוא כלי ייעודי לשליפת נתונים (data fetching) ולניהול מטמון (caching), המגיע כחלק מ-Redux Toolkit. הוא הופך את הדפוס המוכר של useState + useEffect + createAsyncThunk למיותר: מגדירים את נקודות הקצה (endpoints) פעם אחת, ומקבלים Hooks מוכנים שמטפלים עבורכם באופן אוטומטי ב:
- מצבי טעינה ושגיאה (Loading / Error)
- שמירת תוצאות במטמון ושיתופן בין קומפוננטות (Caching)
- רענון נתונים אוטומטי בעת הצורך (Re-fetching)
- עדכונים אופטימיים (Optimistic updates)
הגדרת API
נקודת המוצא היא הגדרת "פרוסת API" בעזרת createApi. בתוכה מגדירים את כתובת הבסיס של השרת ואת נקודות הקצה, המחולקות לשני סוגים: query לקריאת נתונים (GET) ו-mutation לשינוי נתונים (POST/PUT/DELETE). מנגנון ה-tags מאפשר ל-RTK Query לדעת אילו נתונים במטמון להתייתר ולרענן לאחר שינוי. מכל אלה נוצרים אוטומטית ה-Hooks שבהם נשתמש בקומפוננטות:
// store/api/usersApi.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const usersApi = createApi({
reducerPath: 'usersApi',
baseQuery: fetchBaseQuery({
baseUrl: '/api',
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token;
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
return headers;
}
}),
tagTypes: ['User', 'Users'],
endpoints: (builder) => ({
// GET all users
getUsers: builder.query({
query: () => '/users',
providesTags: ['Users']
}),
// GET single user
getUserById: builder.query({
query: (id) => `/users/${id}`,
providesTags: (result, error, id) => [{ type: 'User', id }]
}),
// POST new user
addUser: builder.mutation({
query: (newUser) => ({
url: '/users',
method: 'POST',
body: newUser
}),
invalidatesTags: ['Users']
}),
// PUT update user
updateUser: builder.mutation({
query: ({ id, ...patch }) => ({
url: `/users/${id}`,
method: 'PUT',
body: patch
}),
invalidatesTags: (result, error, { id }) => [
{ type: 'User', id },
'Users'
]
}),
// DELETE user
deleteUser: builder.mutation({
query: (id) => ({
url: `/users/${id}`,
method: 'DELETE'
}),
invalidatesTags: ['Users']
})
})
});
// ייצוא Hooks אוטומטיים
export const {
useGetUsersQuery,
useGetUserByIdQuery,
useAddUserMutation,
useUpdateUserMutation,
useDeleteUserMutation
} = usersApi;
הגדרת Store
// store/store.js
import { configureStore } from '@reduxjs/toolkit';
import { usersApi } from './api/usersApi';
const store = configureStore({
reducer: {
[usersApi.reducerPath]: usersApi.reducer,
// reducers נוספים...
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(usersApi.middleware)
});
export default store;
שימוש - Query (GET)
ה-Hooks מסוג query משמשים לקריאת נתונים. הם מחזירים אובייקט עשיר הכולל את הנתונים עצמם (data), דגלי מצב (isLoading, isError) ופונקציית refetch לרענון ידני - כך שאין צורך לנהל את כל אלה בעצמנו:
import { useGetUsersQuery, useGetUserByIdQuery } from './store/api/usersApi';
function UserList() {
const {
data: users,
isLoading,
isError,
error,
isFetching,
refetch
} = useGetUsersQuery();
if (isLoading) return <p>טוען...</p>;
if (isError) return <p>שגיאה: {error.message}</p>;
return (
<div>
<button onClick={refetch} disabled={isFetching}>
{isFetching ? 'מרענן...' : 'רענן'}
</button>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
function UserProfile({ userId }) {
const { data: user, isLoading } = useGetUserByIdQuery(userId, {
// אפשרויות
skip: !userId, // דלג אם אין userId
pollingInterval: 30000, // רענון כל 30 שניות
refetchOnMountOrArgChange: true
});
if (isLoading) return <p>טוען פרופיל...</p>;
if (!user) return <p>משתמש לא נמצא</p>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
שימוש - Mutation (POST/PUT/DELETE)
ה-Hooks מסוג mutation משמשים לשינוי נתונים בשרת. בשונה מ-query, הם אינם רצים אוטומטית אלא מחזירים פונקציית הפעלה שקוראים לה בעת הצורך. שרשור .unwrap() מאפשר לתפוס הצלחה או שגיאה בעזרת try/catch רגיל:
import {
useAddUserMutation,
useUpdateUserMutation,
useDeleteUserMutation
} from './store/api/usersApi';
function AddUserForm() {
const [addUser, { isLoading, isSuccess, isError, error }] = useAddUserMutation();
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
try {
await addUser({ name, email }).unwrap();
setName('');
setEmail('');
alert('המשתמש נוסף בהצלחה!');
} catch (err) {
alert('שגיאה: ' + err.message);
}
};
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={isLoading}>
{isLoading ? 'שומר...' : 'הוסף משתמש'}
</button>
</form>
);
}
function UserItem({ user }) {
const [updateUser] = useUpdateUserMutation();
const [deleteUser, { isLoading: isDeleting }] = useDeleteUserMutation();
const handleUpdate = async () => {
const newName = prompt('שם חדש:', user.name);
if (newName) {
await updateUser({ id: user.id, name: newName });
}
};
const handleDelete = async () => {
if (confirm('למחוק את המשתמש?')) {
await deleteUser(user.id);
}
};
return (
<li>
{user.name}
<button onClick={handleUpdate}>✏️ ערוך</button>
<button onClick={handleDelete} disabled={isDeleting}>
{isDeleting ? '...' : '❌ מחק'}
</button>
</li>
);
}
Tags ו-Cache Invalidation
מנגנון ה-Tags הוא שמחבר בין קריאות לשינויים. כל query "מספקת" תגית (providesTags), וכל mutation יכולה "לבטל" תגית (invalidatesTags). כשמבטלים תגית, RTK Query מרענן אוטומטית כל query שסיפקה אותה - כך התצוגה נשארת מסונכרנת עם השרת ללא רענון ידני:
// Tags מאפשרים לקשר בין queries ל-mutations
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
providesTags: ['Posts'] // ה-query מספקת תגית 'Posts'
}),
addPost: builder.mutation({
query: (post) => ({ url: '/posts', method: 'POST', body: post }),
invalidatesTags: ['Posts'] // מבטלת את הקאש של 'Posts'
})
})
// כשנקרא addPost, getPosts יתרענן אוטומטית!
Tags מתקדמים
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
providesTags: (result) =>
result
? [
...result.map(({ id }) => ({ type: 'Post', id })),
{ type: 'Post', id: 'LIST' }
]
: [{ type: 'Post', id: 'LIST' }]
}),
updatePost: builder.mutation({
query: ({ id, ...patch }) => ({
url: `/posts/${id}`,
method: 'PATCH',
body: patch
}),
// מבטל רק את הפוסט הספציפי
invalidatesTags: (result, error, { id }) => [{ type: 'Post', id }]
})
})
Optimistic Updates
בעדכון אופטימי מעדכנים את התצוגה מיד, עוד לפני שהשרת הספיק להשיב, כדי שהממשק ירגיש מהיר ותגובתי. אם השרת מחזיר שגיאה, מבטלים את השינוי וחוזרים למצב הקודם. RTK Query מספק לכך את onQueryStarted יחד עם undo:
updatePost: builder.mutation({
query: ({ id, ...patch }) => ({
url: `/posts/${id}`,
method: 'PATCH',
body: patch
}),
async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
// עדכון אופטימיסטי - לפני התשובה מהשרת
const patchResult = dispatch(
postsApi.util.updateQueryData('getPosts', undefined, (draft) => {
const post = draft.find(p => p.id === id);
if (post) {
Object.assign(post, patch);
}
})
);
try {
await queryFulfilled;
} catch {
// אם נכשל - חזור לערך הקודם
patchResult.undo();
}
}
})
סיכום
| Hook | סוג | שימוש |
|---|---|---|
useGetXQuery |
Query | קריאת נתונים |
useXMutation |
Mutation | שינוי נתונים |
providesTags |
- | הגדרת תגיות קאש |
invalidatesTags |
- | ביטול קאש |
תכונות אוטומטיות
- ✅ Loading/Error states
- ✅ Caching
- ✅ Automatic refetch
- ✅ Deduplication
- ✅ Polling
- ✅ Optimistic updates
הקודם: ← Redux Hooks
הבא: Testing →
בדקו את עצמכם
נסו לענות לבד לפני שאתם פותחים את התשובה.
-
מה RTK Query חוסך?
הצגת התשובה
תשובה ג. הוק כמו useGetPostsQuery מחזיר data, isLoading ו-error מוכנים.
-
מה עושה invalidatesTags במוטציה?
הצגת התשובה
תשובה ג. כך הרשימה מתעדכנת אוטומטית אחרי הוספה או מחיקה.