לדלג לתוכן

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), ריאקט:

  1. מעדכנת את ערך הסטייט
  2. קוראת שוב לפונקציית הקומפוננטה (re-render)
  3. הקומפוננטה מחזירה JSX חדש עם הערך המעודכן
  4. ריאקט משווה את ה-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 כשהערך החדש תלוי בקודם
  • הרמת סטייט - כשקומפוננטות צריכות לשתף סטייט, מרימים אותו לאב המשותף