Context API ו-Redux
Context API ו-Redux — ניהול State גלובלי
כשהאפליקציה גדלה, useState לבד לא מספיק. שיעור זה מלמד את שני הכלים המרכזיים לניהול State משותף — Context API המובנה ב-React, ו-Redux Toolkit לאפליקציות גדולות.
- Prop Drilling — הבעיה שגורמת לנו לרצות ניהול State גלובלי
- Context API: createContext + Provider + useContext
- Custom Hook לContext: useUser() במקום useContext(UserContext) בכל מקום
- useReducer: (state, action) => newState — ל-State מורכב ברמת קומפוננטה
- Redux Toolkit: Slice מאחד initialState + Reducers + Actions
- Immer: Redux Toolkit מאפשר שינוי State ישיר — מייצר immutability מאחורי הקלעים
- useSelector ו-useDispatch — קריאה ועדכון Store מכל קומפוננטה
- Selectors עם createSelector — מניעת חישוב מיותר
- מתי Context ומתי Redux — בחירת הכלי הנכון
הבעיה — Prop Drilling
ב-React, State זורם מלמעלה למטה — מאב לבן דרך props. זה עובד מצוין לאפליקציות קטנות. אבל מה קורה כשיש 5 רמות של קומפוננטות?
Prop Drilling = מצב שבו מעבירים prop דרך קומפוננטות שלא צריכות אותו — רק כדי שהוא יגיע לקומפוננטה עמוקה יותר.
// הבעיה: user צריך להגיע ל-Avatar, אבל עובר דרך כולם
function App() {
const [user, setUser] = useState({ name: 'Alice', role: 'admin' });
return <Layout user={user} />; // Layout לא צריך user
}
function Layout({ user }) {
return <Navbar user={user} />; // Navbar לא צריך user
}
function Navbar({ user }) {
return <Avatar user={user} />; // רק Avatar באמת צריך!
}
function Avatar({ user }) {
return <img src={user.avatar} alt={user.name} />;
}! למה Prop Drilling הוא בעיה? ›
- Layout ו-Navbar מקבלים user רק כדי להעביר אותו הלאה — הם לא עושים איתו כלום.
- בכל שינוי ב-user כל הרשרת מתרנדרת מחדש — ביצועים גרועים.
- שינוי מבנה: רוצים להזיז Avatar? צריך לשנות כל הרשרת.
Context API — הפתרון המובנה
Context מאפשר להעביר נתונים לכל הקומפוננטות בעץ — בלי להעביר props ידנית בכל שלב. שלושה שלבים: צור Context, עטוף ב-Provider, קרא עם useContext.
2.1 — יצירה ושימוש בסיסי
import { createContext, useContext, useState } from 'react';
// שלב 1: צור את ה-Context עם ערך ברירת מחדל
const UserContext = createContext(null);
// שלב 2: Provider — עוטף את כל הקומפוננטות שצריכות גישה
function App() {
const [user, setUser] = useState({ name: 'Alice', role: 'admin' });
return (
<UserContext.Provider value={{ user, setUser }}>
<Layout /> {/* לא צריך להעביר user! */}
</UserContext.Provider>
);
}
// שלב 3: כל קומפוננטה קוראת ישירות מה-Context
function Avatar() {
const { user } = useContext(UserContext); // קופץ ישירות ל-user
return <img src={user.avatar} alt={user.name} />;
}2.2 — Custom Hook לContext נקי
כדי לא לכתוב useContext(UserContext) בכל מקום, מכינים Custom Hook:
// contexts/UserContext.js
import { createContext, useContext, useState } from 'react';
const UserContext = createContext(null);
// Provider כ-Component נפרד
export function UserProvider({ children }) {
const [user, setUser] = useState(null);
function login(userData) { setUser(userData); }
function logout() { setUser(null); }
return (
<UserContext.Provider value={{ user, login, logout }}>
{children}
</UserContext.Provider>
);
}
// Custom Hook — הגנה אם משתמשים מחוץ ל-Provider
export function useUser() {
const ctx = useContext(UserContext);
if (!ctx) throw new Error('useUser חייב להיות בתוך UserProvider');
return ctx;
}
// --- שימוש ---
// index.jsx
root.render(<UserProvider><App /></UserProvider>);
// כל קומפוננטה:
function Header() {
const { user, logout } = useUser(); // פשוט!
return <button {user?.name}, התנתק</button>;
}2.3 — מתי Context מספיק ומתי לא
| Context API מתאים כש... | Redux מתאים כש... |
|---|---|
| State שמשותף לחלק מהאפליקציה | State גלובלי לכל האפליקציה |
| עדכונים פשוטים (set/clear) | לוגיקה מורכבת עם הרבה פעולות |
| פרויקט בינוני, צוות קטן | פרויקט גדול, צוות מרובה |
| Theme, User, Language | Shopping Cart, User Auth, API Cache |
! טעות נפוצה — Context גורם לרנדור מחדש ›
כל שינוי ב-value של Provider גורם לכל הקומפוננטות שמשתמשות ב-useContext לרנדר מחדש. לכן לא מכניסים ל-Context נתונים שמשתנים הרבה (כמו scroll position).
useReducer — State מורכב
useReducer הוא Hook מובנה ב-React שמתאים ל-State עם הרבה פעולות שונות. הוא עובד בדיוק כמו Redux — רק ברמת הקומפוננטה.
// useReducer: reducer = פונקציה שמקבלת (state, action) ומחזירה state חדש
// 1. הגדרת Reducer
function todosReducer(state, action) {
switch (action.type) {
case 'ADD':
return [...state, { id: Date.now(), text: action.text, done: false }];
case 'TOGGLE':
return state.map(t => t.id === action.id ? { ...t, done: !t.done } : t);
case 'DELETE':
return state.filter(t => t.id !== action.id);
case 'CLEAR_DONE':
return state.filter(t => !t.done);
default:
return state; // חשוב! תמיד להחזיר state ב-default
}
}
// 2. שימוש ב-Component
function TodoApp() {
const [todos, dispatch] = useReducer(todosReducer, []);
// dispatch שולח Action לReducer
const add = text => dispatch({ type: 'ADD', text });
const toggle = id => dispatch({ type: 'TOGGLE', id });
const del = id => dispatch({ type: 'DELETE', id });
}Redux Toolkit — ניהול State בקנה מידה
Redux הוא ספרייה לניהול State גלובלי — Store אחד מרכזי לכל האפליקציה. Redux Toolkit הוא הגרסה המודרנית שמורידה את ה-Boilerplate לאפס.
| מושג | תיאור |
|---|---|
| Store | אחסון מרכזי יחיד — כל ה-State של האפליקציה |
| Slice | חלק מה-Store: State + Actions + Reducer במקום אחד |
| Action | אובייקט שמתאר מה קרה: { type: 'todos/add', payload: '...' } |
| Reducer | פונקציה טהורה: (state, action) => newState |
| Selector | פונקציה שמוציאה נתון ספציפי מה-Store |
| Dispatch | שליחת Action ל-Store — מפעיל את ה-Reducer |
4.1 — התקנה
npm install @reduxjs/toolkit react-redux4.2 — יצירת Slice
Slice = הגדרת חתיכה אחת מה-Store. מכיל את initialState ואת כל ה-Reducers (שהופכים ל-Actions אוטומטית):
// store/todosSlice.js
import { createSlice } from '@reduxjs/toolkit';
const todosSlice = createSlice({
name: 'todos', // שם ה-Slice — קובע את prefix ל-action types
initialState: [], // State ראשוני
reducers: {
// כל function כאן = Reducer + Action בבת אחת
add(state, action) {
// Redux Toolkit משתמש ב-Immer — מותר לשנות state ישירות!
state.push({ id: Date.now(), text: action.payload, done: false });
},
toggle(state, action) {
const todo = state.find(t => t.id === action.payload);
if (todo) todo.done = !todo.done; // Immer מטפל ב-immutability
},
remove(state, action) {
return state.filter(t => t.id !== action.payload);
},
},
});
// מייצאים את ה-Actions (נוצרו אוטומטית)
export const { add, toggle, remove } = todosSlice.actions;
// מייצאים את ה-Reducer לחיבור ל-Store
export default todosSlice.reducer;4.3 — הגדרת Store וחיבור ל-React
// store/index.js
import { configureStore } from '@reduxjs/toolkit';
import todosReducer from './todosSlice';
import userReducer from './userSlice'; // Slice נוסף
export const store = configureStore({
reducer: {
todos: todosReducer, // state.todos
user: userReducer, // state.user
},
});
// main.jsx — עוטפים הכל ב-Provider
import { Provider } from 'react-redux';
import { store } from './store';
root.render(
<Provider store={store}>
<App />
</Provider>
);4.4 — קריאה ועדכון מקומפוננטות
import { useSelector, useDispatch } from 'react-redux';
import { add, toggle, remove } from './store/todosSlice';
function TodoList() {
// useSelector: קורא נתון ספציפי מה-Store
// קומפוננטה תתרנדר מחדש רק כש-todos משתנה
const todos = useSelector(state => state.todos);
const dispatch = useDispatch();
return (
<div>
<button => dispatch(add('לקנות חלב'))}>הוסף</button>
<ul>
{todos.map(t => (
<li key={t.id}>
<span => dispatch(toggle(t.id))}
style={{ textDecoration: t.done ? 'line-through' : 'none' }}
>
{t.text}
</span>
<button => dispatch(remove(t.id))}>מחק</button>
</li>
))}
</ul>
</div>
);
}const app = express();
app.post('/login', (req, res) => {
console.log(req.body.username);
res.send('Done');
});
app.listen(8080);
Selectors — קריאה חכמה מה-Store
Selector הוא פונקציה שמוציאה (ומחשבת) נתון מה-Store. כך מרכזים את לוגיקת הגישה ל-State במקום אחד, ולא כופלים אותה בכל קומפוננטה.
// store/todosSlice.js — מוסיפים Selectors בסוף הקובץ
// Selector בסיסי
export const selectAllTodos = state => state.todos;
export const selectActiveTodos = state => state.todos.filter(t => !t.done);
export const selectDoneTodos = state => state.todos.filter(t => t.done);
export const selectTodoCount = state => state.todos.length;
// --- שימוש ---
function TodoStats() {
const total = useSelector(selectTodoCount);
const active = useSelector(selectActiveTodos).length;
return <p>{active} מתוך {total} משימות נותרו</p>;
}
// createSelector: Memoized Selector — מחשב מחדש רק כשהקלט משתנה
import { createSelector } from '@reduxjs/toolkit';
export const selectFilteredTodos = createSelector(
[selectAllTodos, state => state.filter],
(todos, filter) => {
if (filter === 'active') return todos.filter(t => !t.done);
if (filter === 'done') return todos.filter(t => t.done);
return todos;
}
);- חישוב ב-Component: todos.filter(...) — רץ בכל render, גם כשלא צריך
- Selector עם createSelector: מחשב מחדש רק כשהנתונים עצמם השתנו
- בסדנה — Selector נראה מקצועי ומראה שאתה מבין ביצועים
Context לעומת Redux — איך בוחרים
אחת השאלות הנפוצות ביותר: מתי להשתמש ב-Context ומתי ב-Redux? התשובה תלויה בגודל הפרויקט ומורכבות ה-State.
| קריטריון | Context API | Redux Toolkit |
|---|---|---|
| גודל פרויקט | קטן-בינוני | בינוני-גדול |
| מורכבות State | פשוטה (user, theme, lang) | מורכבת (cart, auth, filters) |
| DevTools | לא מובנה | Redux DevTools מעולים |
| Boilerplate | מינימלי | קצת יותר (Slice, Store) |
| ביצועים | רנדור מיותר אפשרי | Selector מונע רנדור מיותר |
| Learning Curve | נמוך | בינוני — אך כדאי ללמוד |
| Async Actions | ידני עם useEffect | createAsyncThunk מובנה |
- Auth (user מחובר) + Theme (dark/light) + Language → Context API. מהיר, קל, מספיק.
- Shopping cart + Filters + API Cache + History → Redux Toolkit. שווה את ה-setup.
- ניתן לשלב: Context לנתונים פשוטים, Redux לנתונים מורכבים — זה לגיטימי לחלוטין.
מטלות מעשיות — בניית שרת מאובטח
בחלק זה תבנו שרת Express מאפס שיטפל בפניות של סוכני מודיעין. כל מטלה בונה על הקודמת.
כתבו קוד JavaScript בסיסי (קובץ server.js) המייבא את ספריית express, מפעיל את האפליקציה, ומגדיר את השרת להאזין על פורט 3000. כאשר השרת עולה, הדפיסו לקונסול: 'Secure HQ Server is online on port 3000'. אל תשכחו להוסיף את ה-Middleware שמאפשר קריאת נתוני JSON!
הוסיפו לשרת נתיב GET לכתובת /api/health. כאשר פונים לנתיב זה, השרת יחזיר אובייקט JSON עם: status: 'active' ו-timestamp: Date.now().
סוכנים צריכים לשלוף משימות. צרו נתיב GET דינמי לכתובת /api/missions/:missionId. חלצו את ה-ID מהבקשה והחזירו: { mission: '<ה-ID שהתקבל>', clearanceRequired: 'Top Secret' }.
סוכנים שולחים מידע מהשטח. צרו נתיב POST לכתובת /api/intel שמקבל targetName ו-dangerLevel מה-Body. הדפיסו למסוף והחזירו { success: true, message: 'Intel Received' }.
הגנו על הנתיב ממטלה 4! שנו אותו כך שיבצע ולידציה: אם חסר targetName או אם dangerLevel אינו מספר (או גדול מ-10), החזירו קוד 400 Bad Request עם הודעת שגיאה. רק מידע תקין יקבל 201 Created.
תרגול נוסף — Flask: Decorators, Debug ו-Routing
שאלות העשרה נוספות לחיזוק החומר של השיעור — באותו פורמט בחירה של הקורס.
@wraps(f) ב-Decorator?debug=False מונע ב-Production?GET /api/users/abc ל-Route שמוגדר /api/users/<int:user_id>. מה יקרה?אתגר מעשי — שרת מאובטח — אימות והרשאה (תוספת)
תוספת מעל 5 המטלות הקיימות (הקמת שרת → Trust Boundary): כאן מוסיפים שכבת אימות והרשאה. 5 המטלות הקיימות נשמרות ומקבלות חוזה-בדיקה בבנק ה-JSON.
סיכום — שיעור 19
בשיעור זה כיסינו את הכלים המרכזיים לניהול State גלובלי ב-React. אלו הנושאים שיופיעו בפרויקט הסופי.
- Prop Drilling — הבעיה שגורמת לנו לרצות ניהול State גלובלי
- Context API: createContext + Provider + useContext — שיתוף State בלי props
- Custom Hook לContext: useUser() במקום useContext(UserContext) בכל מקום
- useReducer: (state, action) => newState — ל-State מורכב ברמת קומפוננטה
- Redux Toolkit: Slice מאחד initialState + Reducers + Actions במקום אחד
- Immer: Redux Toolkit מאפשר שינוי State ישיר — מייצר immutability מאחורי הקלעים
- Store: configureStore עם כמה Slices — כל Slice חתיכה אחת מה-State הגלובלי
- useSelector: קורא מה-Store, מתרנדר מחדש רק כשהנתון השתנה
- useDispatch: שולח Action ל-Store — מפעיל את ה-Reducer המתאים
- Selectors: פונקציות לגישה ל-Store, createSelector למניעת חישוב מיותר
- Context לנתונים פשוטים (user, theme) — Redux לנתונים מורכבים (cart, filters)
- הוסף Redux Toolkit ל-Todo App — Slice לטודואים, Slice לפילטר
- צור Context לניהול Theme (dark/light) שמשפיע על כל האפליקציה
- שיעור 20: TypeScript — הוספת Types לכל מה שלמדנו
שאלות תרגול ותשובות (10)
שאלה 1 · איזו בעיה Context API בא לפתור?
- א. Props Drilling — העברת נתונים דרך שכבות שלא צריכות אותם
- ב. איטיות ברינדור
- ג. טעינת נתונים מהשרת
- ד. ניהול ניתוב בין עמודים
Context מאפשר לצרוך ערך ישירות בכל עומק, בלי להעביר props דרך קומפוננטות ביניים.
שאלה 2 · מה חסר כדי ש-useContext יחזיר ערך אמיתי?
const ThemeContext = createContext();
function Child() {
const theme = useContext(ThemeContext);
return <div>{theme}</div>;
}- א. להוסיף useState
- ב. לעטוף את Child ב-ThemeContext.Provider עם value
- ג. לייבא את Child דינמית
- ד. להשתמש ב-useEffect
בלי Provider מקבלים את ערך ברירת המחדל (כאן undefined). ה-Provider מזרים את ה-value לעץ.
שאלה 3 · מה החיסרון העיקרי של Context לנתונים שמשתנים תדיר?
- א. הוא לא נתמך במובייל
- ב. כל צרכני ה-Context מתרנדרים מחדש בכל שינוי ערך
- ג. הוא לא תומך באובייקטים
- ד. הוא איטי בטעינה ראשונה
Context אינו מנגנון אופטימיזציה — שינוי value מרנדר את כל הצרכנים. לכן לסטייט תכוף מעדיפים Redux/Zustand.
שאלה 4 · מה מחזירה פונקציית reducer?
function reducer(state, action) {
switch (action.type) {
case "increment":
return { ...state, count: state.count + 1 };
default:
return state;
}
}- א. אובייקט state חדש
- ב. את ה-action
- ג. ערך בוליאני
- ד. Promise
Reducer הוא פונקציה טהורה: (state, action) => newState, בלי תופעות לוואי ובלי שינוי המצב הקיים.
שאלה 5 · מהם שלושת עקרונות היסוד של Redux?
- א. מקור אמת יחיד, state לקריאה בלבד, שינויים דרך פונקציות טהורות
- ב. Provider, Hook, Component
- ג. Fetch, Cache, Render
- ד. Action, Router, Store
Store יחיד; שינוי רק ע"י dispatch של action; ה-reducers טהורים ודטרמיניסטיים.
שאלה 6 · הקוד הבא בתוך createSlice של Redux Toolkit — האם הוא תקין?
reducers: {
increment: (state) => {
state.count += 1;
}
}- א. לא — אסור לשנות state ישירות
- ב. כן — RTK משתמש ב-Immer שממיר את השינוי לעדכון immutable
- ג. כן, אבל רק במספרים
- ד. לא — חייבים להחזיר ערך
🎯 דגש למבחן — מלכודת ה-Mutation: ב-Redux הקלאסי שינוי ישיר של ה-state הוא באג חמור. ב-Redux Toolkit ספריית Immer עוטפת את ה-state ב-Proxy, כך שהכתיבה "המשנה" מתורגמת מאחורי הקלעים לאובייקט חדש. חשוב לציין בראיון שזה נכון רק בתוך createSlice/createReducer — מחוץ להם עדיין חייבים spread.
שאלה 7 · מה תפקידו של dispatch?
dispatch({ type: "cart/addItem", payload: { id: 7 } });- א. לקרוא ערך מה-store
- ב. לשלוח action ל-store כדי שה-reducer יחשב state חדש
- ג. לרנדר קומפוננטה
- ד. לשמור נתונים בשרת
dispatch הוא הדרך היחידה לשנות state ב-Redux — הוא מזין את ה-action ל-reducers.
שאלה 8 · מה עושה useSelector?
const total = useSelector(state => state.cart.total);
- א. מנוי לפיסת state ורינדור מחדש כשהיא משתנה
- ב. שולח action
- ג. יוצר store חדש
- ד. מנקה את ה-state
useSelector בוחר חתיכת state ומרנדר את הקומפוננטה רק כשהערך שנבחר משתנה.
שאלה 9 · מתי עדיף Redux על פני Context?
- א. כשיש state גלובלי מורכב עם עדכונים תכופים וצורך בכלי דיבאג
- ב. תמיד — Context הוא מיושן
- ג. כשיש רק ערך אחד לשתף
- ד. רק באפליקציות מובייל
לאפליקציה גדולה עם לוגיקה מורכבת Redux נותן מבנה, DevTools ו-time-travel. לערך יציב כמו theme — Context מספיק.
שאלה 10 · למה עדיף לפצל את הבחירה לשדות ספציפיים?
const user = useSelector(state => state.user); // לעומת const name = useSelector(state => state.user.name);
- א. כדי לחסוך זיכרון בשרת
- ב. כדי שרינדור מחדש יקרה רק כשהשדה הרלוונטי משתנה
- ג. כי אובייקטים אסורים ב-Redux
- ד. כדי לעקוף את Immer
בחירה רחבה מדי גורמת לרינדור בכל שינוי בענף כולו; בחירה ממוקדת מצמצמת רינדורים מיותרים.