Redux Hooks ופעולות אסינכרוניות
מבוא
לאחר שהקמנו את ה-store וה-slices, הגיע הזמן להעמיק בעבודה השוטפת מול Redux מתוך הקומפוננטות. שיעור זה מכסה:
- שימוש מעמיק ב-
useSelectorוב-useDispatch - פעולות אסינכרוניות (כמו קריאות שרת) בעזרת
createAsyncThunk - Middleware - שכבת ביניים בזרימת ה-actions
- דפוסי עבודה מתקדמים ומומלצים
useSelector - קריאה מה-Store
ה-Hook בשם useSelector מאפשר לקומפוננטה לקרוא נתון מתוך ה-store. הוא מקבל פונקציית "בורר" (selector) שבוחרת את החלק הרלוונטי מה-State, והקומפוננטה תתרנדר מחדש רק כשאותו חלק ישתנה. כדי לא לגרום לרינדורים מיותרים, עדיף לבחור ערכים ספציפיים ולא להחזיר אובייקט חדש בכל קריאה:
import { useSelector } from 'react-redux';
function MyComponent() {
// קריאה ישירה
const count = useSelector(state => state.counter.value);
// שימוש ב-selector מוגדר מראש
const todos = useSelector(selectAllTodos);
// Selector עם חישוב
const total = useSelector(state =>
state.cart.items.reduce((sum, item) => sum + item.price, 0)
);
// מספר ערכים (לא מומלץ - גורם לרינדורים מיותרים)
const { user, isLoading } = useSelector(state => ({
user: state.auth.user,
isLoading: state.auth.isLoading
}));
}
Selectors עם Reselect
import { createSelector } from '@reduxjs/toolkit';
// Selector בסיסי
const selectItems = state => state.cart.items;
const selectTaxRate = state => state.settings.taxRate;
// Selector מורכב עם memoization
const selectTotalWithTax = createSelector(
[selectItems, selectTaxRate],
(items, taxRate) => {
const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0);
return subtotal * (1 + taxRate);
}
);
// שימוש
function CartTotal() {
const total = useSelector(selectTotalWithTax);
return <p>סה"כ: ₪{total.toFixed(2)}</p>;
}
useDispatch - שליחת Actions
אם useSelector משמש לקריאה מה-store, useDispatch משמש לכתיבה אליו. הוא מחזיר את פונקציית ה-dispatch, שבאמצעותה שולחים actions כדי לעדכן את ה-State:
import { useDispatch } from 'react-redux';
import { increment, addTodo } from './store';
function MyComponent() {
const dispatch = useDispatch();
// שליחה פשוטה
const handleClick = () => {
dispatch(increment());
};
// שליחה עם payload
const handleAdd = (text) => {
dispatch(addTodo(text));
};
// שליחה מרובה
const handleReset = () => {
dispatch(resetCart());
dispatch(clearUser());
dispatch(showNotification('הנתונים אופסו'));
};
}
createAsyncThunk - פעולות אסינכרוניות
reducers רגילים חייבים להיות סינכרוניים, אך פעולות רבות (כמו שליפת נתונים מהשרת) לוקחות זמן. createAsyncThunk נועד בדיוק לכך: הוא עוטף פעולה אסינכרונית ומייצר עבורה שלושה actions אוטומטיים - pending (התחלה), fulfilled (הצלחה) ו-rejected (כישלון). את שלושתם מטפלים ב-slice דרך extraReducers, וכך מנהלים בקלות מצבי טעינה ושגיאה:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// יצירת async thunk
export const fetchUsers = createAsyncThunk(
'users/fetchUsers', // שם ה-action
async (_, { rejectWithValue }) => {
try {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('שגיאה בטעינה');
}
return await response.json();
} catch (error) {
return rejectWithValue(error.message);
}
}
);
// עם פרמטרים
export const fetchUserById = createAsyncThunk(
'users/fetchById',
async (userId, { rejectWithValue }) => {
try {
const response = await fetch(`/api/users/${userId}`);
return await response.json();
} catch (error) {
return rejectWithValue(error.message);
}
}
);
// עם גישה ל-state
export const addToCart = createAsyncThunk(
'cart/addItem',
async (product, { getState, rejectWithValue }) => {
const { auth } = getState();
if (!auth.user) {
return rejectWithValue('יש להתחבר תחילה');
}
const response = await fetch('/api/cart', {
method: 'POST',
headers: {
'Authorization': `Bearer ${auth.token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(product)
});
return await response.json();
}
);
טיפול ב-Slice
const usersSlice = createSlice({
name: 'users',
initialState: {
items: [],
selectedUser: null,
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null
},
reducers: {
clearError: (state) => {
state.error = null;
}
},
// טיפול ב-async actions
extraReducers: (builder) => {
builder
// fetchUsers
.addCase(fetchUsers.pending, (state) => {
state.status = 'loading';
state.error = null;
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.status = 'succeeded';
state.items = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload || action.error.message;
})
// fetchUserById
.addCase(fetchUserById.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchUserById.fulfilled, (state, action) => {
state.status = 'succeeded';
state.selectedUser = action.payload;
})
.addCase(fetchUserById.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload;
});
}
});
שימוש בקומפוננטה
function UserList() {
const dispatch = useDispatch();
const { items: users, status, error } = useSelector(state => state.users);
useEffect(() => {
if (status === 'idle') {
dispatch(fetchUsers());
}
}, [status, dispatch]);
if (status === 'loading') {
return <p>טוען משתמשים...</p>;
}
if (status === 'failed') {
return <p>שגיאה: {error}</p>;
}
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Middleware
Middleware הוא שכבת ביניים שכל action עובר דרכה בדרך אל ה-reducer. זו נקודת הידוק אידיאלית לפעולות רוחביות כמו רישום ללוג (logging), דיווח שגיאות או ניתוב פעולות אסינכרוניות:
// store/middleware/logger.js
const loggerMiddleware = (store) => (next) => (action) => {
console.group(action.type);
console.log('dispatching:', action);
console.log('prev state:', store.getState());
const result = next(action);
console.log('next state:', store.getState());
console.groupEnd();
return result;
};
// store/store.js
import { configureStore } from '@reduxjs/toolkit';
const store = configureStore({
reducer: { /* ... */ },
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(loggerMiddleware)
});
Middleware לדיווח שגיאות
const errorReportingMiddleware = (store) => (next) => (action) => {
try {
return next(action);
} catch (error) {
console.error('Error in reducer:', error);
// שליחה לשירות דיווח שגיאות
reportError(error, action);
throw error;
}
};
Typed Hooks (TypeScript)
// store/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';
// Typed hooks
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
// שימוש
function MyComponent() {
const dispatch = useAppDispatch();
const user = useAppSelector(state => state.auth.user); // עם autocomplete!
}
דפוסי שימוש מתקדמים
Action Creator עם לוגיקה
// Thunk ידני (לא createAsyncThunk)
export const conditionalIncrement = () => (dispatch, getState) => {
const { value } = getState().counter;
if (value < 10) {
dispatch(increment());
}
};
export const incrementIfOdd = (amount) => (dispatch, getState) => {
const { value } = getState().counter;
if (value % 2 === 1) {
dispatch(incrementByAmount(amount));
}
};
Batch Updates
import { batch } from 'react-redux';
function handleSubmit() {
// כל ה-dispatches יגרמו לרינדור אחד בלבד
batch(() => {
dispatch(updateUser(userData));
dispatch(updateSettings(settings));
dispatch(showNotification('נשמר בהצלחה'));
});
}
סיכום
| API | שימוש |
|---|---|
useSelector(selector) |
קריאה מה-store |
useDispatch() |
קבלת dispatch |
createAsyncThunk |
async actions |
extraReducers |
טיפול ב-thunk |
createSelector |
memoized selectors |
| Middleware | לוגיקה בין dispatch ל-reducer |
הקודם: ← Redux Basics
הבא: RTK Query →
בדקו את עצמכם
נסו לענות לבד לפני שאתם פותחים את התשובה.
-
איך קוראים נתון מה-store בקומפוננטה?
הצגת התשובה
תשובה ד. useSelector בוחר חלק מה-state ומרנדר מחדש כשהוא משתנה.
-
איך שולחים action?
הצגת התשובה
תשובה א. useDispatch מחזיר את dispatch, ושולחים לו action.