6.4 סטייט הרצאה
מה זה סטייט - State¶
סטייט הוא הזיכרון הפנימי של קומפוננטה. בעוד ש-props מגיעים מבחוץ ואי אפשר לשנות אותם, סטייט הוא נתון שהקומפוננטה מנהלת בעצמה ויכולה לשנות. כששטייט משתנה, ריאקט מרנדרת מחדש את הקומפוננטה עם הערך החדש.
בלי סטייט, קומפוננטות הן סטטיות - הן מציגות מה שקיבלו ב-props ותו לא. סטייט מאפשר אינטראקטיביות.
useState - הוק הסטייט¶
useState הוא הוק (hook) שמאפשר להוסיף סטייט לקומפוננטה:
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
useState מחזיר מערך עם שני איברים:
1. הערך הנוכחי של הסטייט (count)
2. פונקציה לעדכון הסטייט (setCount)
הארגומנט של useState הוא הערך ההתחלתי (במקרה שלנו, 0).
טיפוסים עם useState¶
טייפסקריפט מסיקה את הטיפוס מהערך ההתחלתי:
const [count, setCount] = useState(0); // number
const [name, setName] = useState("Alice"); // string
const [isOpen, setIsOpen] = useState(false); // boolean
כשהטיפוס לא ניתן להסקה (למשל מתחילים עם null), צריך לציין אותו:
const [user, setUser] = useState<User | null>(null);
const [items, setItems] = useState<string[]>([]);
איך עובד רינדור מחדש - Re-render¶
כשקוראים לפונקציית העדכון (כמו setCount), ריאקט:
- מעדכנת את ערך הסטייט
- קוראת שוב לפונקציית הקומפוננטה (re-render)
- הקומפוננטה מחזירה JSX חדש עם הערך המעודכן
- ריאקט משווה את ה-JSX החדש לישן ומעדכנת רק את מה שהשתנה ב-DOM
function Toggle() {
const [isOn, setIsOn] = useState(false);
// this entire function runs again on every re-render
console.log("Toggle rendered, isOn:", isOn);
return (
<button onClick={() => setIsOn(!isOn)}>
{isOn ? "ON" : "OFF"}
</button>
);
}
חשוב: משתנים רגילים (let/const) מתאפסים בכל רינדור. רק סטייט שמור בין רינדורים:
function BrokenCounter() {
let count = 0; // resets to 0 on every render!
return (
<button onClick={() => { count++; console.log(count); }}>
Count: {count} {/* always shows 0 */}
</button>
);
}
חוסר שינוי - Immutability¶
בריאקט, אסור לשנות סטייט ישירות. תמיד צריך ליצור ערך חדש ולהעביר אותו לפונקציית העדכון:
// WRONG - mutating state directly
const [user, setUser] = useState({ name: "Alice", age: 30 });
const handleBirthday = () => {
user.age += 1; // BAD! mutating the existing object
setUser(user); // React won't re-render - same reference
};
// CORRECT - creating a new object
const handleBirthday = () => {
setUser({ ...user, age: user.age + 1 }); // new object with updated age
};
ריאקט משווה references כדי לדעת אם הסטייט השתנה. אם מעבירים את אותו אובייקט, ריאקט חושבת שלא השתנה כלום.
עדכון אובייקטים¶
כשהסטייט הוא אובייקט, משתמשים ב-spread operator כדי ליצור עותק חדש:
interface FormData {
firstName: string;
lastName: string;
email: string;
}
function ProfileForm() {
const [form, setForm] = useState<FormData>({
firstName: "",
lastName: "",
email: "",
});
const handleChange = (field: keyof FormData, value: string) => {
setForm({ ...form, [field]: value });
};
return (
<div>
<input
value={form.firstName}
onChange={(e) => handleChange("firstName", e.target.value)}
placeholder="First name"
/>
<input
value={form.lastName}
onChange={(e) => handleChange("lastName", e.target.value)}
placeholder="Last name"
/>
<input
value={form.email}
onChange={(e) => handleChange("email", e.target.value)}
placeholder="Email"
/>
</div>
);
}
אובייקטים מקוננים¶
interface Address {
street: string;
city: string;
}
interface User {
name: string;
address: Address;
}
const [user, setUser] = useState<User>({
name: "Alice",
address: { street: "Main St", city: "Tel Aviv" },
});
// updating a nested property - spread at every level
setUser({
...user,
address: { ...user.address, city: "Haifa" },
});
עדכון מערכים¶
מערכים גם דורשים יצירת עותק חדש:
function TodoList() {
const [todos, setTodos] = useState<string[]>([]);
// add item
const addTodo = (text: string) => {
setTodos([...todos, text]); // new array with the old items + new one
};
// remove item by index
const removeTodo = (index: number) => {
setTodos(todos.filter((_, i) => i !== index));
};
// update item by index
const updateTodo = (index: number, newText: string) => {
setTodos(todos.map((todo, i) => (i === index ? newText : todo)));
};
return (
<div>
<button onClick={() => addTodo("New task")}>Add</button>
<ul>
{todos.map((todo, index) => (
<li key={index}>
{todo}
<button onClick={() => removeTodo(index)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
פעולות נפוצות על מערכים¶
// add to end
setItems([...items, newItem]);
// add to beginning
setItems([newItem, ...items]);
// remove by index
setItems(items.filter((_, i) => i !== index));
// remove by id
setItems(items.filter((item) => item.id !== idToRemove));
// update by id
setItems(items.map((item) => (item.id === id ? { ...item, done: true } : item)));
// sort (create a new array first!)
setItems([...items].sort((a, b) => a.name.localeCompare(b.name)));
אל תשתמשו ב-push, pop, splice, sort ישירות - הם משנים את המערך המקורי. תמיד צרו מערך חדש.
עדכון פונקציונלי - Functional Updates¶
כשהערך החדש תלוי בערך הקודם, עדיף להשתמש בצורה הפונקציונלית:
// might have stale value if called multiple times quickly
setCount(count + 1);
// always uses the latest value
setCount((prev) => prev + 1);
מתי זה חשוב? כשקוראים ל-setter כמה פעמים ברצף:
function Counter() {
const [count, setCount] = useState(0);
const incrementThree = () => {
// WRONG - all three use the same 'count' value
setCount(count + 1); // count is 0, sets to 1
setCount(count + 1); // count is still 0, sets to 1
setCount(count + 1); // count is still 0, sets to 1
// result: count = 1, not 3!
// CORRECT - each uses the latest value
setCount((prev) => prev + 1); // prev = 0, sets to 1
setCount((prev) => prev + 1); // prev = 1, sets to 2
setCount((prev) => prev + 1); // prev = 2, sets to 3
// result: count = 3
};
return <button onClick={incrementThree}>+3 (count: {count})</button>;
}
כלל אצבע: כשהערך החדש מבוסס על הערך הקודם, השתמשו בצורה הפונקציונלית prev => newValue.
הרמת סטייט - Lifting State Up¶
כששתי קומפוננטות צריכות לשתף סטייט, מרימים את הסטייט לאב המשותף הקרוב ביותר:
function TemperatureConverter() {
const [celsius, setCelsius] = useState(0);
// both components need this value
const fahrenheit = celsius * 9 / 5 + 32;
return (
<div>
<CelsiusInput value={celsius} onChange={setCelsius} />
<FahrenheitDisplay value={fahrenheit} />
</div>
);
}
interface CelsiusInputProps {
value: number;
onChange: (value: number) => void;
}
function CelsiusInput({ value, onChange }: CelsiusInputProps) {
return (
<label>
Celsius:
<input
type="number"
value={value}
onChange={(e) => onChange(Number(e.target.value))}
/>
</label>
);
}
interface FahrenheitDisplayProps {
value: number;
}
function FahrenheitDisplay({ value }: FahrenheitDisplayProps) {
return <p>Fahrenheit: {value.toFixed(1)}</p>;
}
הסטייט (celsius) חי ב-TemperatureConverter ומועבר למטה דרך props. CelsiusInput משנה אותו דרך callback. FahrenheitDisplay רק מציג ערך מחושב.
העיקרון: source of truth יחיד¶
כל חתיכת מידע צריכה להיות מנוהלת במקום אחד בלבד. אם שתי קומפוננטות צריכות את אותו מידע, המקום שלו הוא באב המשותף - לא כעותק בכל קומפוננטה.
סיכום¶
- סטייט הוא הזיכרון הפנימי של קומפוננטה, מנוהל עם
useState - שינוי סטייט גורם לרינדור מחדש של הקומפוננטה
- חוסר שינוי (immutability) - תמיד יוצרים ערך חדש, לא משנים את הקיים
- עדכון אובייקטים עם spread:
{ ...obj, key: newValue } - עדכון מערכים עם map/filter/spread, לא עם push/splice
- עדכון פונקציונלי
prev => newValueכשהערך החדש תלוי בקודם - הרמת סטייט - כשקומפוננטות צריכות לשתף סטייט, מרימים אותו לאב המשותף