GammaCyber

React: קומפוננטות, State ו-Props

תכנות, אלגוריתמיקה ו-Web · 5–6 שעות

// סקירה

שיעור 17 — React: קומפוננטות, State ו-Props

React שינתה את עולם ה-Web ב-2013. הרעיון: UI = f(State) — שנה State וה-UI מתעדכן אוטומטית. אתה מתאר מה ה-UI צריך להיראות, React דואג לאיך.

✓נושאי השיעור
  • Virtual DOM + Reconciliation — למה React מהיר יותר מ-DOM ידני
  • JSX — תחביר JS+HTML, className, camelCase, expressions
  • Functional Components + Props — קומפוננטות, children, read-only props
  • 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 — כללים ומלכודות

JSXHTML רגילהסבר
classNameclassclass = reserved word ב-JS
htmlForforfor = reserved word ב-JS
onClickonclickcamelCase לכל 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>; }
✏תרגול — useEffect4 שאלות
שאלה 1
useEffect(() => { fetch("/api"); }, [userId, filter]); — מתי רץ?
✓ תשובה: ב) — [userId, filter]: React בודק שניהם. אם אחד השתנה → useEffect ירוץ שוב. תמיד גם בMount.
שאלה 2 — async useEffect
מה שגוי?
useEffect(async () => { const data = await fetch("/api").then(r=>r.json()); setData(data); }, []);
✓ תשובה: א) — תיקון: הגדר async function פנימית וקרא לה: useEffect(() => { async function load() {...} load(); }, [])
שאלה 3 — כמה פעמים
כמה פעמים useEffect עם [count] ירוץ כש-count עולה מ-0 ל-3?
✓ תשובה: א) 4 — Mount(פעם 1) + count: 0→1(פעם 2) + 1→2(פעם 3) + 2→3(פעם 4) = 4.
שאלה 4 — Cleanup
מתי Cleanup function של useEffect רצה?
✓ תשובה: ג) — Cleanup חיוני. ללא: Memory Leak (setInterval ממשיך לרוץ, event listeners נצברים). חובה ל: setInterval, addEventListener, fetch/WebSocket.
// פרקים 6–8

Lists, Keys, Lifting State ו-React Router

Keys הן חובה ברשימות. Lifting State Up הוא הפתרון לשיתוף State. React Router = SPA ללא reload.

Conditional Rendering + Lists

// Conditional Rendering — 4 שיטות: {isLoggedIn ? <Dashboard /> : <Login />} // Ternary {isAdmin && <AdminPanel />} // && (זהירות עם 0!) if (!user) return <p>Please log in.</p>; // Early Return // Lists עם .map() + key חובה! function TodoList({ todos }) { return ( <ul> {todos.map(todo => ( <li key={todo.id}>{todo.text}</li> // key={todo.id} ✓ ))} </ul> ); } // key — כללים: // ✅ key={todo.id} — Unique ו-Stable // ❌ key={index} — בעייתי אם הרשימה זזה/מסוננת // ❌ key={Math.random()}— חדש כל render = React יבנה הכל מחדש

Lifting State Up

// שתי קומפוננטות צריכות אותו query: function SearchPage() { const [query, setQuery] = useState(""); // State באב! return ( <div> <SearchBar value={query} /> <ResultsList query={query} /> </div> ); } // SearchBar ו-ResultsList מקבלים props — לא מנהלים State בעצמם

React Router

import { BrowserRouter, Routes, Route, Link, useParams, useNavigate } from "react-router-dom"; function App() { return ( <BrowserRouter> <nav><Link to="/users">Users</Link></nav> <Routes> <Route path="/" element={<Home />} /> <Route path="/users" element={<UserList />} /> <Route path="/users/:id" element={<UserDetail />} /> <Route path="*" element={<NotFound />} /> </Routes> </BrowserRouter> ); } function UserDetail() { const { id } = useParams(); // /users/42 → id = "42" const navigate = useNavigate(); return <button => navigate(-1)}>Back</button>; }
💡 BrowserRouter vs HashRouter
BrowserRouter: URLs נקיות (/about) — דורש הגדרת שרת. HashRouter: URLs עם # (#/about) — עובד ב-GitHub Pages ללא backend. לVite: BrowserRouter. ל-GitHub Pages ללא backend: HashRouter.
✏תרגול — Lists, Routing4 שאלות
שאלה 1 — key={index}
items.map((item, i) => <li key={i}>{item}</li>)
✓ תשובה: ג) — key={index} בעייתי: אם מוחקים/מסדרים, React מזהה אלמנטים שגוי. עדיף key={item.id} ייחודי ויציב.
שאלה 2 — Unmount
כשקומפוננטה מאנמאונטת (יוצאת מה-tree), מה קורה ל-state שלה?
✓ תשובה: ב) — Unmount = מחיקת כל ה-state. Re-mount = initialState מחדש. זה גם למה Cleanup חשוב — למנוע effects שממשיכים אחרי Unmount.
שאלה 3 — useParams
Route: path="/users/:id", URL: /users/42. מה מחזיר useParams()?
✓ תשובה: ב) {id: "42"} — useParams מחזיר string! אם צריך number: const numId = Number(id). Route path=".../:id" → key = "id".
שאלה 4 — Rules of Hooks
מה כלל Rules of Hooks לגבי מיקום useState/useEffect?
✓ תשובה: ב) — React סומך על סדר קבוע של Hooks בין renders. אם תשים useState בתוך if → שגיאה "Rendered more hooks than in the previous render".
// פרק 9 + סיכום

Todo App מלא וסיכום

Todo App מלא עם useState, useEffect, localStorage, Lifting State Up, ו-3 קומפוננטות.

קוד מלאTodo App — App.jsx ›
function App() { // Lazy initial state — קרא מ-localStorage פעם אחת const [todos, setTodos] = useState(() => { const saved = localStorage.getItem("todos"); return saved ? JSON.parse(saved) : []; }); const [filter, setFilter] = useState("all"); // Persist to localStorage כשtodos משתנה useEffect(() => { localStorage.setItem("todos", JSON.stringify(todos)); }, [todos]); function addTodo(text) { setTodos(prev => [...prev, { id: Date.now(), text, done: false }]); } function toggleTodo(id) { setTodos(prev => prev.map(t => t.id===id ? {...t, done:!t.done} : t)); } function deleteTodo(id) { setTodos(prev => prev.filter(t => t.id !== id)); } const filtered = todos.filter(t => filter==="active" ? !t.done : filter==="done" ? t.done : true ); return ( <div> <AddTodo /> <FilterBar filter={filter} /> <ul>{filtered.map(t => <TodoItem key={t.id} todo={t} /> )}</ul> </div> ); }
✓מה למדנו — שיעור 17
  • Virtual DOM: Diffing + Patching. לא rebuild מלא.
  • JSX: className, camelCase, Expressions ב-{}. Fragment (<>) ללא wrapper.
  • useState: setter מחליף הכל (לא merge). Updater function למניעת Stale State. לא לעשות Mutation!
  • useEffect: 3 מצבים. Cleanup חובה ל-setInterval/addEventListener. לא async ישירות.
  • Keys: חובה ברשימות. לא index, לא random. id ייחודי ויציב.
  • Lifting State Up: State באב המשותף, props לילדים.
  • React Router: Link (SPA), useParams (URL params), useNavigate.
  • Hooks Rules: תמיד top level. לא בתוך conditions/loops.
🎯 שיעור הבא — שיעור 18
Fetch ו-APIs — חיבור React ל-Backend: Fetch API, Axios, useFetch Custom Hook, Loading/Error States, Optimistic Updates.
✏תרגול סיכום — גאמא סטייל4 שאלות
סיכום 1 — Derived State
מה derived state?
✓ תשובה: ד) — דוגמה: const count = todos.filter(t => !t.done).length — לא צריך useState נוסף! שמירת ערך נגזר ב-state = Sync Bug.
סיכום 2 — lazy useState
useState(() => JSON.parse(localStorage.getItem("data") || "[]"))
✓ תשובה: ד) — 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
דוגמאות
apply_actions({'count':0}, [{'type':'inc','by':1}, {'type':'inc','by':2}]) → [{'count': 1}, {'count': 3}]
הפתרון נשמר מקומית ומשותף עם עמוד „כתיבת קוד”.
פתרון-ייחוס תיקון באג — אל תשנה 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'} מסיר את כל שהושלמו. החזר את המצב הסופי.
דוגמאות
add 'x', add 'y', toggle id=1, clear_done → {'todos': [{'id':2,'text':'y','done':False}], 'nextId': 3}
הפתרון נשמר מקומית ומשותף עם עמוד „כתיבת קוד”.
פתרון-ייחוס useReducer — ניהול רשימת משימות ›
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, לא משנה אותם.

שאלה 3 · מה יהיה ערך count אחרי הלחיצה?

const [count, setCount] = useState(0);

function handleClick() {
  setCount(count + 1);
  setCount(count + 1);
}
  • א. 2
  • ב. 1
  • ג. 0
  • ד. שגיאה
תשובה: ב

count "קפוא" ברינדור הנוכחי — שתי הקריאות מחשבות 0+1. הפתרון: setCount(c => c + 1).

שאלה 4 · מה קורה כשמשמיטים את מערך התלויות ב-useEffect?

useEffect(() => {
  fetchData();
});
  • א. רץ פעם אחת בלבד
  • ב. רץ אחרי כל רינדור
  • ג. לא רץ כלל
  • ד. רץ רק כשה-props משתנים
תשובה: ב

ללא מערך תלויות האפקט רץ אחרי כל רינדור — ואם הוא מעדכן state נוצרת לולאה אינסופית.

שאלה 5 · מתי רצה פונקציית ה-cleanup שמוחזרת מ-useEffect?

useEffect(() => {
  const id = setInterval(tick, 1000);
  return () => clearInterval(id);
}, []);
  • א. לפני האפקט הבא ובעת הסרת הקומפוננטה
  • ב. רק בטעינה הראשונה
  • ג. רק כשנזרקת שגיאה
  • ד. אחרי כל שינוי state
תשובה: א

ה-cleanup מנקה מנויים/טיימרים לפני הרצה חוזרת ובעת unmount — כך נמנעת דליפת זיכרון.

שאלה 6 · מה יודפס בלוג אחרי 3 שניות?

const [count, setCount] = useState(0);

function start() {
  setCount(5);
  setTimeout(() => {
    console.log(count);
  }, 3000);
}
  • א. 5
  • ב. 0
  • ג. undefined
  • ד. שגיאה
תשובה: ב

ה-closure של setTimeout תפס את count של הרינדור הישן — 0. 🎯 דגש למבחן — מלכודת ה-Stale State: כשמעדכנים state בתוך setTimeout, interval או event handler ישן, אל תסתמכו על המשתנה עצמו — השתמשו ב-Updater Function: setCount(prev => prev + 1). זו התשובה שמצפים לשמוע בראיון.

שאלה 7 · למה אסור להשתמש באינדקס כ-key ברשימה דינמית?

{items.map((item, i) => <Row key={i} data={item} />)}
  • א. האינדקס איטי לחישוב
  • ב. React דורש key ייחודי מסוג מחרוזת
  • ג. בהוספה/מחיקה באמצע האינדקסים זזים ו-React מתבלבל
  • ד. האינדקס לא נשמר בזיכרון
תשובה: ג

key מזהה פריט בין רינדורים. אינדקס משתנה בשינוי הסדר → state של שורות מתערבב. השתמשו ב-id יציב.

שאלה 8 · מהי הדרך הנכונה לעדכן שדה באובייקט state?

const [user, setUser] = useState({ name: "Bob", age: 30 });
  • א. user.age = 31
  • ב. setUser({ ...user, age: 31 })
  • ג. setUser(user.age = 31)
  • ד. user = { ...user, age: 31 }
תשובה: ב

State הוא immutable — יוצרים אובייקט חדש. מוטציה ישירה לא מפעילה רינדור כי ההפניה לא השתנתה.

שאלה 9 · מהו Lifting State Up?

  • א. העברת state לקומפוננטת האב המשותפת
  • ב. שמירת state ב-localStorage
  • ג. שימוש ב-useRef במקום useState
  • ד. פיצול קומפוננטה לשני קבצים
תשובה: א

כששתי קומפוננטות אחיות צריכות את אותו מידע — ה-state עולה לאב המשותף ויורד אליהן כ-props.

שאלה 10 · מה ההבדל בין useRef ל-useState?

  • א. useRef מהיר יותר בחישוב
  • ב. שינוי useRef לא מפעיל רינדור מחדש
  • ג. useRef עובד רק על אלמנטי DOM
  • ד. useState לא נשמר בין רינדורים
תשובה: ב

useRef הוא "תיבה" שנשמרת בין רינדורים בלי לגרום לרינדור — מתאים לטיימרים, מזהים והפניות ל-DOM.