useState — הסוד לרי-רנדר, updater function, Stale State
useEffect — Side Effects, dependency array, Cleanup
Conditional Rendering + Lists + Keys (חוב!)
Lifting State Up + Controlled Components
React Router — SPA, useParams, useNavigate
Todo App מלא עם localStorage
// פרקים 1–2
Virtual DOM ו-JSX
Virtual DOM = עץ JS בזיכרון שמייצג את ה-DOM האמיתי. JSX = תחביר שנראה HTML אבל הוא JS.
Virtual DOM — Reconciliation
// תהליך Reconciliation:
// 1. State משתנה → useState setter נקרא
// 2. React מריץ את הפונקציה מחדש (re-render)
// 3. נוצר Virtual DOM חדש
// 4. Diffing: מה השתנה בין ישן לחדש?
// 5. רק השינויים המינימליים מוחלים על ה-Real DOM (Patching)
// למה זה חכם?
// גישה ל-Real DOM = יקרה (Reflow, Repaint)
// Virtual DOM = object JS בלבד — זול מאוד
// Batching: React מאגד עדכונים ומחיל פעם אחת
JSX — כללים ומלכודות
JSX
HTML רגיל
הסבר
className
class
class = reserved word ב-JS
htmlFor
for
for = reserved word ב-JS
onClick
onclick
camelCase לכל event handlers
style={{color:"red"}}
style="color:red"
Object לא string. {{}} = JSX expr + object
<br />
<br>
JSX: self-closing חובה
// JSX מתורגם ל-React.createElement():
const el = <div className="card"><h1>Hello, {name}!</h1></div>;
// →
React.createElement("div", {className:"card"},
React.createElement("h1", null, "Hello, ", name, "!"));
// כללים חשובים:
// 1. חייב להחזיר element אחד בשורש — עטוף ב-<> (Fragment)
// 2. Expressions בלבד בתוך {} — אין if/for
// 3. false, null, undefined לא מרונדרים
// שגוי:
return <div>{if (x) <span>hi</span>}</div> // SyntaxError!
// נכון:
return <div>{x ? <span>hi</span> : null}</div>
return <div>{x && <span>hi</span>}</div>
⚠️ {0 && <Comp />} — מפתיע!
אם count=0, הביטוי {count && <p>{count}</p>} ידפיס "0" — כי 0 הוא falsy אבל React מרנדר אותו! תיקון: {count > 0 && <p>{count}</p>}
✏תרגול — Virtual DOM + JSX3 שאלות
שאלה 1
מה יקרה כש-React מבצע re-render?
✓ תשובה: א) — React: Virtual DOM Diffing → Patch מינימלי על Real DOM. לא rebuild מלא.
שאלה 2
מהו הפלט של: {0 && <span>Hello</span>}
✓ תשובה: ב) "0" — 0 הוא falsy, אבל הוא מספר — React מרנדר מספרים! תיקון: count > 0 && או !!count &&
שאלה 3
מה ההבדל בין <> ל-<React.Fragment>?
✓ תשובה: ד) — Fragment לא יוצר element ב-DOM. כשצריך key ברשימה — חייב <React.Fragment key=...>.
// פרקים 3–4
Functional Components, Props ו-useState
קומפוננטה = פונקציה שמקבלת props ומחזירה JSX. useState = הלב שגורם לרי-רנדר.
Components + Props
function Button({ label, onClick, variant = "primary", disabled = false }) {
return (
<button className={`btn btn-${variant}`} disabled={disabled}>
{label}
</button>
);
}
// שימוש:
<Button label="Save" />
<Button label="Delete" variant="danger" />
// חוקי קומפוננטות:
// 1. שם מתחיל באות גדולה (Button, לא button)
// 2. מחזירה JSX (או null)
// 3. Props = read-only — אסור לשנות!
// 4. Pure function — אותן props = אותו פלט
// children prop — כל מה שבין תגי פתיחה/סגירה:
function Card({ children, title }) {
return <div><h2>{title}</h2>{children}</div>;
}
useState + Stale State
const [count, setCount] = useState(0);
// ⚠️ Stale State — הטעות הנפוצה ביותר:
function addThree() {
setCount(count + 1); // count = 0
setCount(count + 1); // count עדיין 0! (stale)
setCount(count + 1); // count עדיין 0!
// תוצאה: count = 1, לא 3!
}
// ✅ תיקון — updater function:
function addThreeFixed() {
setCount(p => p + 1); // p = latest
setCount(p => p + 1); // p = prev + 1
setCount(p => p + 1); // תוצאה: count = 3 ✓
}
// State עם Array — תמיד Array חדש, לא mutation!
setTodos(prev => [...prev, { id: Date.now(), text, done: false }]); // הוספה
setTodos(prev => prev.filter(t => t.id !== id)); // מחיקה
setTodos(prev => prev.map(t => t.id===id ? {...t, done:!t.done} : t)); // עדכון
✏תרגול — Components + useState4 שאלות
שאלה 1 — Infinite Loop
function Comp() {
const [x, setX] = useState(5);
setX(x + 1); // ← מה קורה?
return <p>{x}</p>;
}
✓ תשובה: ב) — setX בגוף הקומפוננטה (לא בevent/effect) גורם לrender נוסף שגורם לsetX נוסף... אינסוף. תמיד setX בתוך event handler או useEffect!
שאלה 2 — Stale State
// count מתחיל ב-0. לאחר לחיצה = ?
setCount(count + 1); // ×2
setCount(p => p + 1); // ×2
✓ תשובה: ד) 3 — שתי קריאות count+1 עם count=0 → מעדכנות ל-1 פעם אחת (stale!). שתי prev→prev+1 מוסיפות 2. סה"כ: 1+2=3.
שאלה 3 — State array mutation
מה שגוי? const [items, setItems] = useState([]);
function addItem(item) {
items.push(item); // ← ?
setItems(items);
}
✓ תשובה: ב) — React משווה references. items.push = אותו reference → React לא יגלה שינוי ולא יעשה re-render. תמיד: setItems(prev => [...prev, item])
שאלה 4 — setUser שגוי
const [user, setUser] = useState({name:"Alice"});
setUser({age: 25}); // מה יהיה user?
✓ תשובה: א) {age:25} — useState setter מחליף את כל ה-state (לא merge!). תיקון: setUser(prev => ({...prev, age:25}))
// פרק 5
useEffect — Side Effects ו-Cleanup
useEffect מריץ קוד אחרי render. 3 מצבים: ללא array = כל render. [] = פעם אחת. [deps] = כשdep משתנה.
3 מצבי useEffect
// 1. ללא dependency array — רץ אחרי כל render (⚠️ נדיר):
useEffect(() => { console.log("Every render"); });
// 2. [] ריק — רץ פעם אחת בלבד (Mount):
useEffect(() => {
fetchData(); // כמו componentDidMount
}, []);
// 3. [deps] — רץ כשdep משתנה (ובMount):
useEffect(() => {
fetchUser(userId); // רץ כשuserId משתנה
}, [userId]);
// Cleanup — חייב כאן:
useEffect(() => {
const timer = setInterval(() => setTime(new Date()), 1000);
return () => clearInterval(timer); // ← Cleanup!
}, []);
// Cleanup רץ: (1) לפני effect הבא, (2) כשהקומפוננטה מוסרת
// ללא cleanup: Memory Leak!
Fetch ב-useEffect — Pattern מלא
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false; // Race Condition protection!
async function fetchUser() {
try {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
if (!cancelled) setUser(data); // ← בדיקת cancelled
} catch (err) {
if (!cancelled) setError(err.message);
} finally {
if (!cancelled) setLoading(false);
}
}
fetchUser();
return () => { cancelled = true; }; // Cleanup
}, [userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return null;
return <h1>{user.name}</h1>;
}
✓ תשובה: ד) — Lazy Init: הfunction רצה פעם אחת בלבד (חשוב כשהחישוב יקר: JSON.parse, localStorage). ללא lazy — JSON.parse רץ בכל render!
סיכום 3 — effects trace
Mount → לחיצה "Set A" (a=1) → לחיצה "Set B" (b=1). כמה פעמים useEffect([a]) ירוץ?
✓ תשובה: א) 2 — פעם 1: Mount. פעם 2: a=1 (a השתנה). לחיצה על b: a לא השתנה → useEffect([a]) לא רץ.
סיכום 4 — Controlled Component
מה controlled component?
✓ תשובה: ג) — <input value={state} /> = Controlled. React הוא single source of truth. מאפשר ולידציה בזמן אמת.
// אתגר מעשי
אתגר מעשי — React: State ו-Reducers
שיא השיעור: לממש את הלוגיקה שמאחורי ה-Web — פרסור, אירועים, state ואבטחה. פתרו את החובה ואז את האתגר; ההגשה נבדקת אוטומטית מול מקרי-קצה נסתרים.
T2 · בסיסי200 נק'~15 דק'debug
תיקון באג — אל תשנה state (immutability)
הפונקציה מדמה עדכוני state ב-React ומחזירה היסטוריה של המצבים אחרי כל פעולה. אך היא שוברת את כלל-הזהב של React: היא *משנה* את אותו אובייקט state במקום ליצור חדש, ולכן כל רשומות ההיסטוריה מצביעות לאותו אובייקט ומראות את הערך הסופי. תקן כך שכל צעד יפיק עותק חדש (state בלתי-משתנה).
// הקוד השבור לתיקון
def apply_actions(initial, actions):
state = initial; history = []
for a in actions:
if a['type'] == 'inc': state['count'] += a.get('by', 1)
elif a['type'] == 'reset': state['count'] = 0
history.append(state) # באג כאן
return history
פתרון-ייחוס תיקון באג — אל תשנה state (immutability) ›
def apply_actions(initial, actions):
state = dict(initial); history = []
for a in actions:
state = dict(state) # התיקון: עותק חדש בכל צעד (אי-שינוי state)
if a['type'] == 'inc': state['count'] += a.get('by', 1)
elif a['type'] == 'reset': state['count'] = 0
history.append(state)
return history
T3 · בינוני350 נק'~25 דק'algorithmic
useReducer — ניהול רשימת משימות
ממש reducer בסגנון useReducer של React לניהול רשימת משימות. מצב התחלתי: {'todos': [], 'nextId': 1}. עבד רשימת actions לפי הסדר: {'type':'add','text':t} מוסיף משימה {'id':nextId,'text':t,'done':False} ומקדם nextId · {'type':'toggle','id':i} הופך done · {'type':'remove','id':i} מסיר · {'type':'clear_done'} מסיר את כל שהושלמו. החזר את המצב הסופי.
def run_reducer(actions):
state = {'todos': [], 'nextId': 1}
for a in actions:
ty = a['type']
if ty == 'add':
state['todos'].append({'id': state['nextId'], 'text': a['text'], 'done': False})
state['nextId'] += 1
elif ty == 'toggle':
for td in state['todos']:
if td['id'] == a['id']: td['done'] = not td['done']
elif ty == 'remove':
state['todos'] = [td for td in state['todos'] if td['id'] != a['id']]
elif ty == 'clear_done':
state['todos'] = [td for td in state['todos'] if not td['done']]
return state
שאלות תרגול ותשובות (10)
שאלה 1 · מה יקרה כשלוחצים על הכפתור?
function App() {
let count = 0;
return <button onClick={() => count++}>{count}</button>;
}
א. המספר יעלה בכל לחיצה
ב. המספר יישאר 0 — משתנה רגיל לא מפעיל רינדור
ג. הקומפוננטה תזרוק שגיאה
ד. הדף ירוענן בכל לחיצה
תשובה: ב
רק עדכון state דרך useState מפעיל רינדור מחדש. משתנה מקומי נדרס בכל רינדור.
שאלה 2 · למה props נחשבים read-only?
א. הם נשמרים בזיכרון הדפדפן
ב. הם מוגדרים כ-const אוטומטית
ג. הזרימה היא חד-כיוונית — רק ההורה משנה אותם
ד. הם מומרים למחרוזת
תשובה: ג
זרימת מידע חד-כיוונית: הילד מקבל props ומדווח למעלה דרך callback, לא משנה אותם.
ה-closure של setTimeout תפס את count של הרינדור הישן — 0.
🎯 דגש למבחן — מלכודת ה-Stale State: כשמעדכנים state בתוך setTimeout, interval או event handler ישן, אל תסתמכו על המשתנה עצמו — השתמשו ב-Updater Function: setCount(prev => prev + 1). זו התשובה שמצפים לשמוע בראיון.
שאלה 7 · למה אסור להשתמש באינדקס כ-key ברשימה דינמית?