GammaCyber

Context API ו-Redux

פיתוח Fullstack מתקדם · 5–6 שעות

// סקירה

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 — בחירת הכלי הנכון
// פרק 1

הבעיה — 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? צריך לשנות כל הרשרת.
✏תרגול עצמי2 שאלות
שאלה 1.1
מדוע ה-Backend נחשב ל-'Trust Boundary' הראשי, בעוד ה-Frontend אינו כזה?
✓ תשובה: ב) — לעולם אין לסמוך על לקוח (Frontend). המשתמש מחזיק בשליטה מלאה בדפדפן ויכול לבטל בדיקות תקינות בקלות. השרת הוא הסביבה המבודדת היחידה שנמצאת בשליטת המפתחים.
שאלה 1.2
מה הגדרת Prop Drilling?
✓ תשובה: ב) — זוהי בדיוק הבעיה ש-Context API ו-Redux פותרים. הקומפוננטות האמצעיות (Layout, Navbar) מקבלות props שאין להן שימוש בהן.
// פרק 2

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} />; }
💡 הכלל: Context לא מחליף State
Context הוא מנגנון להעברת נתונים, לא לאחסון שלהם. ה-State עצמו עדיין חי ב-useState (או useReducer) בתוך ה-Provider. Context רק מאפשר לכל קומפוננטה להגיע לאותו State בלי prop drilling.

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, LanguageShopping Cart, User Auth, API Cache
! טעות נפוצה — Context גורם לרנדור מחדש ›

כל שינוי ב-value של Provider גורם לכל הקומפוננטות שמשתמשות ב-useContext לרנדר מחדש. לכן לא מכניסים ל-Context נתונים שמשתנים הרבה (כמו scroll position).

פתרון: מפצלים Context לחלקים קטנים, או עוברים ל-Redux עם Selector.
✏תרגול עצמי3 שאלות
שאלה 2.1
מה הסדר הנכון ליצירת Context ב-React?
✓ תשובה: ג) — קודם יוצרים את ה-Context עם createContext, אחר כך עוטפים עם Provider כדי לספק את הערך, ולבסוף כל קומפוננטה שצריכה קוראת עם useContext.
שאלה 2.2
מה היתרון של Custom Hook כמו useUser() לעומת useContext(UserContext) ישירות?
✓ תשובה: א) — Custom Hook עוטף את ה-Context ומוסיף validation (if (!ctx) throw), כך שאם קומפוננטה תנסה לקרוא Context מחוץ ל-Provider תקבל שגיאה ברורה.
שאלה 2.3 — מיקרו-למידה
מה יקרה אם נכניס ל-Context של אפליקציה את ה-scrollPosition (שמשתנה בכל גלילה)?
✓ תשובה: ד) — כל שינוי ב-value גורם לכל הצרכנים לרנדר מחדש. נתונים כמו scroll position, mouse coordinates וכו' לא שייכים ל-Context.
// פרק 3

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 }); }
הרחבה useState לעומת useReducer — מתי כל אחד? ›
useStateuseReducer
ערך פשוט, שינוי אחדState מורכב עם הרבה פעולות
const [count, setCount] = useState(0)שינויים תלויים ב-type של פעולה
כשה-handler פשוטכשה-handler מכיל if/switch על סוג פעולה

כלל אצבע: אם ה-handler מכיל if/switch על סוג פעולה — useReducer.

✏תרגול עצמי2 שאלות
שאלה 3.1
מה חייב ה-Reducer להחזיר ב-case default?
✓ תשובה: ב) — ה-Reducer חייב תמיד להחזיר state. אם ה-action לא מוכר, נחזיר את ה-state הנוכחי ללא שינוי. החזרת null/undefined תגרום לבאגים קשים.
שאלה 3.2
נתון קוד: dispatch({ type: 'ADD', text: 'לקנות חלב' }). מה ה-Reducer יקבל כפרמטרים?
✓ תשובה: ג) — ה-Reducer תמיד מקבל שני פרמטרים: ה-state הנוכחי, וה-action שנשלח. הוא מחזיר את ה-state החדש.
// פרק 4

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-redux

4.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;
הרחבה Immer — הקסם שמאחורי Redux Toolkit ›

בדרך כלל אסור לשנות State ישירות — חייבים להחזיר אובייקט חדש. Redux Toolkit כולל את ספריית Immer שמאפשרת לכתוב state.push() ו-todo.done = true.

Immer "מציצה" לשינויים ויוצרת אובייקט חדש מאחורי הקלעים — כתיבה נוחה, immutability אמיתית.

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> ); }
✏תרגול עצמי4 שאלות
שאלה 4.1
נתון קוד השרת הבא. כאשר לקוח שולח בקשת POST עם JSON הכולל 'username', המסוף מדפיס undefined. מדוע?
const express = require('express');
const app = express();
app.post('/login', (req, res) => {
console.log(req.body.username);
res.send('Done');
});
app.listen(8080);
✓ תשובה: ד) — Express אינו מניח מראש איזה סוג תוכן יגיע. כדי לפענח JSON מגוף הבקשה, חייבים להוסיף app.use(express.json()) בראש הקובץ.
שאלה 4.2
מה ההבדל בין dispatch(add('טקסט')) לבין dispatch({ type: 'todos/add', payload: 'טקסט' })?
✓ תשובה: ד) — Redux Toolkit יוצר Action Creator אוטומטי לכל Reducer. add('טקסט') מחזיר בדיוק { type: 'todos/add', payload: 'טקסט' }. השימוש ב-Action Creator קריא יותר ומונע שגיאות כתיב.
שאלה 4.3
מה יקרה אם נשנה state ישירות ב-Reducer רגיל (לא Redux Toolkit)? state.push(action.payload); return state;
✓ תשובה: ב) — ב-Redux רגיל React משווה references. אם אתה מחזיר את אותו אובייקט (גם אחרי push), ה-reference זהה — Redux לא יזהה שינוי ולא יגרום לרנדור. ב-Redux Toolkit בלבד Immer מטפל בזה.
שאלה 4.4
למה useSelector עדיף על store.getState() ישירות?
✓ תשובה: א) — store.getState() מחזיר snapshot ברגע הקריאה ולא מתעדכן. useSelector גם משווה את תוצאת ה-Selector — רנדור מחדש רק אם התוצאה השתנתה.
// פרק 5

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; } );
🔑חשוב לגאמא: Selector עדיף על חישוב ב-Component
  • חישוב ב-Component: todos.filter(...) — רץ בכל render, גם כשלא צריך
  • Selector עם createSelector: מחשב מחדש רק כשהנתונים עצמם השתנו
  • בסדנה — Selector נראה מקצועי ומראה שאתה מבין ביצועים
✏תרגול עצמי2 שאלות
שאלה 5.1
מה היתרון של createSelector לעומת Selector רגיל?
✓ תשובה: א) — Memoization פירושו שהחישוב נשמר. אם הקלט לא השתנה, הפונקציה מחזירה את אותו תוצאה ממטמון בלי לחשב מחדש. זה חוסך render מיותר.
שאלה 5.2
בקשת רשת נכנסת — כיצד היא יודעת להגיע לאפליקציית Node.js שלנו ולא לשרת הדואר?
✓ תשובה: א) — ה-IP הוא כתובת הבניין, הפורט (Port) הוא הדלת הספציפית. כל תוכנה מאזינה על פורט שונה: שרת דואר על 25, שרת HTTP על 80/443, Node.js על 3000.
// פרק 6

Context לעומת Redux — איך בוחרים

אחת השאלות הנפוצות ביותר: מתי להשתמש ב-Context ומתי ב-Redux? התשובה תלויה בגודל הפרויקט ומורכבות ה-State.

קריטריוןContext APIRedux Toolkit
גודל פרויקטקטן-בינוניבינוני-גדול
מורכבות Stateפשוטה (user, theme, lang)מורכבת (cart, auth, filters)
DevToolsלא מובנהRedux DevTools מעולים
Boilerplateמינימליקצת יותר (Slice, Store)
ביצועיםרנדור מיותר אפשריSelector מונע רנדור מיותר
Learning Curveנמוךבינוני — אך כדאי ללמוד
Async Actionsידני עם useEffectcreateAsyncThunk מובנה
🔑כלל האצבע לסדנה
  • Auth (user מחובר) + Theme (dark/light) + Language → Context API. מהיר, קל, מספיק.
  • Shopping cart + Filters + API Cache + History → Redux Toolkit. שווה את ה-setup.
  • ניתן לשלב: Context לנתונים פשוטים, Redux לנתונים מורכבים — זה לגיטימי לחלוטין.
✏תרגול עצמי2 שאלות
שאלה 6.1 — אבטחה
מערכת login מעבירה פרטי משתמש בבקשת GET: /api/auth?user=admin&pass=123456. מדוע זה חור אבטחה חמור, גם אם האתר מוצפן ב-HTTPS?
✓ תשובה: ד) — בקשות GET, גם כשמוצפנות בתעבורה, עדיין נשמרות בשלמותן בהיסטוריה המקומית, בקובצי Logs ובראוטרים. POST מעביר נתונים בגוף הבקשה — לא נראה בכתובת, לא נשמר בהיסטוריה.
שאלה 6.2
אתה בונה אפליקציית Todo עם: רשימת משימות, פילטר (הכל/פעיל/הושלם), משתמש מחובר, ו-dark mode. מה הגישה המומלצת?
✓ תשובה: א) — שילוב הוא הגישה הנכונה! משימות + פילטר = מורכב + הרבה פעולות → Redux. משתמש + dark mode = פשוט, נדיר משתנה → Context. זו ארכיטקטורה מקצועית.
// פרק 7

מטלות מעשיות — בניית שרת מאובטח

בחלק זה תבנו שרת Express מאפס שיטפל בפניות של סוכני מודיעין. כל מטלה בונה על הקודמת.

מטלה 1
הקמת תשתית השרת

כתבו קוד JavaScript בסיסי (קובץ server.js) המייבא את ספריית express, מפעיל את האפליקציה, ומגדיר את השרת להאזין על פורט 3000. כאשר השרת עולה, הדפיסו לקונסול: 'Secure HQ Server is online on port 3000'. אל תשכחו להוסיף את ה-Middleware שמאפשר קריאת נתוני JSON!

פתרון מטלה 1 ›
const express = require('express'); const app = express(); const PORT = 3000; // חובה! בלי זה req.body יהיה undefined app.use(express.json()); app.listen(PORT, () => { console.log(`Secure HQ Server is online on port ${PORT}`); });
מטלה 2
בדיקת בריאות המערכת (Healthcheck Route)

הוסיפו לשרת נתיב GET לכתובת /api/health. כאשר פונים לנתיב זה, השרת יחזיר אובייקט JSON עם: status: 'active' ו-timestamp: Date.now().

פתרון מטלה 2 ›
app.get('/api/health', (req, res) => { res.status(200).json({ status: 'active', timestamp: Date.now() }); });
מטלה 3
שליפה דינמית (Dynamic Params)

סוכנים צריכים לשלוף משימות. צרו נתיב GET דינמי לכתובת /api/missions/:missionId. חלצו את ה-ID מהבקשה והחזירו: { mission: '<ה-ID שהתקבל>', clearanceRequired: 'Top Secret' }.

פתרון מטלה 3 ›
app.get('/api/missions/:missionId', (req, res) => { const requestedId = req.params.missionId; // חילוץ ה-ID res.status(200).json({ mission: requestedId, clearanceRequired: 'Top Secret' }); });
💡 מיקרו-למידה — Dynamic Route Params: במקום לכתוב נתיב נפרד לכל /missions/1, /missions/2... נגדיר :missionId ונגיש ב-req.params.missionId.
מטלה 4
קליטת מודיעין (POST Route & Body)

סוכנים שולחים מידע מהשטח. צרו נתיב POST לכתובת /api/intel שמקבל targetName ו-dangerLevel מה-Body. הדפיסו למסוף והחזירו { success: true, message: 'Intel Received' }.

פתרון מטלה 4 ›
app.post('/api/intel', (req, res) => { const { targetName, dangerLevel } = req.body; console.log(`[!] Intel received on ${targetName}, Level: ${dangerLevel}`); res.status(200).json({ success: true, message: 'Intel Received' }); });
מטלה 5
יישום Trust Boundary — ולידציה מתקדמת

הגנו על הנתיב ממטלה 4! שנו אותו כך שיבצע ולידציה: אם חסר targetName או אם dangerLevel אינו מספר (או גדול מ-10), החזירו קוד 400 Bad Request עם הודעת שגיאה. רק מידע תקין יקבל 201 Created.

פתרון מלא כל 5 המטלות ›
// server.js — פתרון מלא משולב const express = require('express'); const app = express(); const PORT = 3000; // מטלה 1: Middleware לפענוח JSON app.use(express.json()); // מטלה 2: Healthcheck app.get('/api/health', (req, res) => { res.status(200).json({ status: 'active', timestamp: Date.now() }); }); // מטלה 3: Dynamic Params app.get('/api/missions/:missionId', (req, res) => { const requestedId = req.params.missionId; res.status(200).json({ mission: requestedId, clearanceRequired: 'Top Secret' }); }); // מטלות 4 + 5: POST עם ולידציה (Trust Boundary) app.post('/api/intel', (req, res) => { const { targetName, dangerLevel } = req.body; // מטלה 5: ולידציה — Trust Boundary if (!targetName || typeof dangerLevel !== 'number' || dangerLevel > 10) { return res.status(400).json({ error: 'Invalid intel format. Target name is required and danger must be 0-10.' }); } // מטלה 4: מידע תקין — הדפסה ואישור console.log(`[!] Intel received on ${targetName}, Level: ${dangerLevel}`); res.status(201).json({ success: true, message: 'Intel Received securely' }); }); // מטלה 1: האזנה על הפורט app.listen(PORT, () => { console.log(`Secure HQ Server is online on port ${PORT}`); });
// תרגול נוסף

תרגול נוסף — Flask: Decorators, Debug ו-Routing

שאלות העשרה נוספות לחיזוק החומר של השיעור — באותו פורמט בחירה של הקורס.

✏תרגול נוסף3 שאלות
שאלה 7.1 [קל]
מה עושה @wraps(f) ב-Decorator?
✓ תשובה נכונה: ג — @wraps מעתיק __name__, __doc__ מ-f ל-wrapper. קריטי ב-Flask: בלעדיו כל ה-Routes מתנגשים בשם 'wrapper' ונופלים.
שאלה 7.2 [בינוני]
מה debug=False מונע ב-Production?
✓ תשובה נכונה: א — debug=True מציג Werkzeug traceback אינטראקטיבי — פנינה לתוקף (קוד, נתיבים, ואף console). ב-Production: תמיד debug=False.
שאלה 7.3 [קשה]
משתמש שולח GET /api/users/abc ל-Route שמוגדר /api/users/<int:user_id>. מה יקרה?
✓ תשובה נכונה: ד — הממיר <int:> תואם ספרות בלבד. 'abc' לא תואם → אין Route מתאים → 404 אוטומטי, עוד לפני שהפונקציה רצה. Type-safe routing.
// אתגר מעשי

אתגר מעשי — שרת מאובטח — אימות והרשאה (תוספת)

תוספת מעל 5 המטלות הקיימות (הקמת שרת → Trust Boundary): כאן מוסיפים שכבת אימות והרשאה. 5 המטלות הקיימות נשמרות ומקבלות חוזה-בדיקה בבנק ה-JSON.

T2 · בסיסי300 נק'~20 דק'algorithmic
אימות טוקן חתום (HMAC)
לפני שמאבטחים endpoint צריך לאמת זהות. טוקן בפורמט "payload.signature", כאשר signature = HMAC-SHA256(secret, payload) ב-hex. ממש verify_token(token, secret) שמחזיר True רק אם החתימה תקפה. השתמשו בהשוואת זמן-קבוע (hmac.compare_digest) כדי למנוע timing attack. פורמט שגוי → False.
דוגמאות
verify_token(valid_token, secret) → True
verify_token(tampered_token, secret) → False
verify_token('noDotHere', secret) → False
הפתרון נשמר מקומית ומשותף עם עמוד „כתיבת קוד”.
פתרון-ייחוס אימות טוקן חתום (HMAC) ›
import hmac, hashlib def verify_token(token: str, secret: str) -> bool: if token.count('.') != 1: return False payload, sig = token.rsplit('.', 1) expected = hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest() return hmac.compare_digest(expected, sig) # השוואה בזמן-קבוע (נגד timing attack)
T3 · בינוני450 נק'~30 דק'build
שכבת הרשאה מלאה (Auth + Trust Boundary)
ממש את מקבל-ההחלטות של ה-endpoint המאובטח כפונקציה טהורה (שפה-אגנוסטית, לבדיקה אוטומטית). authorize(request) מקבל {method, path, headers, body}. עבור POST /api/intel: דרוש כותרת Authorization: Bearer <token> תקף (HMAC, כמו במשימה הקודמת) — אחרת (401, {error:'Unauthorized'}). לאחר מכן החל Trust Boundary: targetName מחרוזת לא-ריקה ו-dangerLevel מספר ≤10 — אחרת (400, {error:'Invalid intel'}). קלט תקין → (201, {success:True}). נתיב לא-מוכר → (404, {error:'Not found'}).
דוגמאות
POST /api/intel + Bearer תקף + body תקין → (201, {'success': True})
POST /api/intel בלי טוקן → (401, {'error': 'Unauthorized'})
POST /api/intel + טוקן תקף + dangerLevel=99 → (400, {'error': 'Invalid intel'})
הפתרון נשמר מקומית ומשותף עם עמוד „כתיבת קוד”.
פתרון-ייחוס שכבת הרשאה מלאה (Auth + Trust Boundary) ›
import hmac, hashlib SECRET = "hq-secret" def _verify(token): if token.count('.') != 1: return False p, sig = token.rsplit('.', 1) exp = hmac.new(SECRET.encode(), p.encode(), hashlib.sha256).hexdigest() return hmac.compare_digest(exp, sig) def authorize(request: dict): method, path = request.get('method'), request.get('path') headers, body = request.get('headers', {}), request.get('body', {}) if path == '/api/intel' and method == 'POST': auth = headers.get('Authorization', '') if not auth.startswith('Bearer ') or not _verify(auth[7:]): return (401, {'error': 'Unauthorized'}) # שכבת אימות tn, dl = body.get('targetName'), body.get('dangerLevel') if not isinstance(tn, str) or not tn: return (400, {'error': 'Invalid intel'}) if isinstance(dl, bool) or not isinstance(dl, (int, float)) or dl > 10: return (400, {'error': 'Invalid intel'}) # Trust Boundary return (201, {'success': True}) return (404, {'error': 'Not found'})
// סיכום

סיכום — שיעור 19

בשיעור זה כיסינו את הכלים המרכזיים לניהול State גלובלי ב-React. אלו הנושאים שיופיעו בפרויקט הסופי.

✓מה למדנו בשיעור 19
  • 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)
📌 לפני שיעור 20 (TypeScript)
  1. הוסף Redux Toolkit ל-Todo App — Slice לטודואים, Slice לפילטר
  2. צור Context לניהול Theme (dark/light) שמשפיע על כל האפליקציה
  3. שיעור 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
תשובה: ב

בחירה רחבה מדי גורמת לרינדור בכל שינוי בענף כולו; בחירה ממוקדת מצמצמת רינדורים מיותרים.