לדלג לתוכן

6.4 סטייט פתרון

פתרון - סטייט

פתרון תרגיל 1

import { useState } from "react";

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

    return (
        <div>
            <h2>Count: {count}</h2>
            <button onClick={() => setCount(count + 1)}>+1</button>
            <button onClick={() => setCount(count - 1)} disabled={count === 0}>
                -1
            </button>
            <button onClick={() => setCount(0)}>Reset</button>
        </div>
    );
}

שימו לב לשימוש ב-disabled={count === 0} כדי למנוע ירידה מתחת ל-0.

פתרון תרגיל 2

import { useState } from "react";

function TodoList() {
    const [todos, setTodos] = useState<string[]>([]);
    const [input, setInput] = useState("");

    const addTodo = () => {
        if (input.trim()) {
            setTodos([...todos, input.trim()]);
            setInput("");
        }
    };

    const removeTodo = (index: number) => {
        setTodos(todos.filter((_, i) => i !== index));
    };

    return (
        <div>
            <h2>Tasks ({todos.length})</h2>
            <div>
                <input
                    value={input}
                    onChange={(e) => setInput(e.target.value)}
                    placeholder="New task..."
                    onKeyDown={(e) => e.key === "Enter" && addTodo()}
                />
                <button onClick={addTodo}>Add</button>
            </div>
            <ul>
                {todos.map((todo, index) => (
                    <li key={index}>
                        {todo}
                        <button onClick={() => removeTodo(index)}>Delete</button>
                    </li>
                ))}
            </ul>
        </div>
    );
}

הוספנו גם תמיכה ב-Enter בשדה הטקסט כדי להוסיף משימה.

פתרון תרגיל 3

import { useState } from "react";

interface FormData {
    username: string;
    email: string;
    age: number;
    newsletter: boolean;
}

function RegistrationForm() {
    const [form, setForm] = useState<FormData>({
        username: "",
        email: "",
        age: 0,
        newsletter: false,
    });

    const updateField = (field: keyof FormData, value: string | number | boolean) => {
        setForm({ ...form, [field]: value });
    };

    return (
        <div>
            <h2>Registration</h2>
            <div>
                <label>
                    Username:
                    <input
                        value={form.username}
                        onChange={(e) => updateField("username", e.target.value)}
                    />
                </label>
            </div>
            <div>
                <label>
                    Email:
                    <input
                        type="email"
                        value={form.email}
                        onChange={(e) => updateField("email", e.target.value)}
                    />
                </label>
            </div>
            <div>
                <label>
                    Age:
                    <input
                        type="number"
                        value={form.age}
                        onChange={(e) => updateField("age", Number(e.target.value))}
                    />
                </label>
            </div>
            <div>
                <label>
                    <input
                        type="checkbox"
                        checked={form.newsletter}
                        onChange={(e) => updateField("newsletter", e.target.checked)}
                    />
                    Subscribe to newsletter
                </label>
            </div>

            <h3>Preview:</h3>
            <p>Username: {form.username || "(empty)"}</p>
            <p>Email: {form.email || "(empty)"}</p>
            <p>Age: {form.age}</p>
            <p>Newsletter: {form.newsletter ? "Yes" : "No"}</p>
        </div>
    );
}

פתרון תרגיל 4

import { useState } from "react";

interface Contact {
    id: number;
    name: string;
    phone: string;
    isFavorite: boolean;
}

function ContactList() {
    const [contacts, setContacts] = useState<Contact[]>([]);
    const [name, setName] = useState("");
    const [phone, setPhone] = useState("");
    const [nextId, setNextId] = useState(1);

    const addContact = () => {
        if (name.trim() && phone.trim()) {
            setContacts([
                ...contacts,
                { id: nextId, name: name.trim(), phone: phone.trim(), isFavorite: false },
            ]);
            setNextId(nextId + 1);
            setName("");
            setPhone("");
        }
    };

    const removeContact = (id: number) => {
        setContacts(contacts.filter((c) => c.id !== id));
    };

    const toggleFavorite = (id: number) => {
        setContacts(
            contacts.map((c) => (c.id === id ? { ...c, isFavorite: !c.isFavorite } : c))
        );
    };

    const favoriteCount = contacts.filter((c) => c.isFavorite).length;

    return (
        <div>
            <h2>Contacts ({contacts.length}) - Favorites: {favoriteCount}</h2>
            <div>
                <input
                    value={name}
                    onChange={(e) => setName(e.target.value)}
                    placeholder="Name"
                />
                <input
                    value={phone}
                    onChange={(e) => setPhone(e.target.value)}
                    placeholder="Phone"
                />
                <button onClick={addContact}>Add Contact</button>
            </div>
            <ul>
                {contacts.map((contact) => (
                    <li key={contact.id}>
                        <span style={{ fontWeight: contact.isFavorite ? "bold" : "normal" }}>
                            {contact.name} - {contact.phone}
                        </span>
                        <button onClick={() => toggleFavorite(contact.id)}>
                            {contact.isFavorite ? "Unfavorite" : "Favorite"}
                        </button>
                        <button onClick={() => removeContact(contact.id)}>Delete</button>
                    </li>
                ))}
            </ul>
        </div>
    );
}

פתרון תרגיל 5

import { useState } from "react";

function BatchCounter() {
    const [regularCount, setRegularCount] = useState(0);
    const [functionalCount, setFunctionalCount] = useState(0);

    const incrementRegular = () => {
        setRegularCount(regularCount + 1);
        setRegularCount(regularCount + 1);
        setRegularCount(regularCount + 1);
        // result: +1, because all three use the same 'regularCount' value
    };

    const incrementFunctional = () => {
        setFunctionalCount((prev) => prev + 1);
        setFunctionalCount((prev) => prev + 1);
        setFunctionalCount((prev) => prev + 1);
        // result: +3, because each uses the latest value
    };

    const reset = () => {
        setRegularCount(0);
        setFunctionalCount(0);
    };

    return (
        <div style={{ display: "flex", gap: "40px" }}>
            <div>
                <h3>Regular: {regularCount}</h3>
                <button onClick={incrementRegular}>+3 (regular)</button>
            </div>
            <div>
                <h3>Functional: {functionalCount}</h3>
                <button onClick={incrementFunctional}>+3 (functional)</button>
            </div>
            <button onClick={reset}>Reset both</button>
        </div>
    );
}

כשלוחצים על "+3 (רגיל)", המונה עולה ב-1 בלבד. כשלוחצים על "+3 (פונקציונלי)", המונה עולה ב-3. זה מדגים למה עדכון פונקציונלי חשוב כשהערך החדש תלוי בקודם.

פתרון תרגיל 6

import { useState } from "react";

interface Product {
    id: number;
    name: string;
    price: number;
}

interface CartItem {
    product: Product;
    quantity: number;
}

const products: Product[] = [
    { id: 1, name: "Laptop", price: 999 },
    { id: 2, name: "Mouse", price: 29 },
    { id: 3, name: "Keyboard", price: 79 },
    { id: 4, name: "Monitor", price: 399 },
];

interface ProductCatalogProps {
    onAddToCart: (product: Product) => void;
}

function ProductCatalog({ onAddToCart }: ProductCatalogProps) {
    return (
        <div>
            <h2>Products</h2>
            {products.map((product) => (
                <div key={product.id} style={{ display: "flex", gap: "8px", alignItems: "center", marginBottom: "8px" }}>
                    <span>{product.name} - ${product.price}</span>
                    <button onClick={() => onAddToCart(product)}>Add to Cart</button>
                </div>
            ))}
        </div>
    );
}

interface ShoppingCartProps {
    items: CartItem[];
}

function ShoppingCart({ items }: ShoppingCartProps) {
    const total = items.reduce((sum, item) => sum + item.product.price * item.quantity, 0);

    return (
        <div>
            <h2>Shopping Cart</h2>
            {items.length === 0 ? (
                <p>Cart is empty</p>
            ) : (
                <>
                    <ul>
                        {items.map((item) => (
                            <li key={item.product.id}>
                                {item.product.name} x{item.quantity} - ${item.product.price * item.quantity}
                            </li>
                        ))}
                    </ul>
                    <p><strong>Total: ${total}</strong></p>
                </>
            )}
        </div>
    );
}

function App() {
    const [cart, setCart] = useState<CartItem[]>([]);

    const handleAddToCart = (product: Product) => {
        setCart((prev) => {
            const existing = prev.find((item) => item.product.id === product.id);
            if (existing) {
                return prev.map((item) =>
                    item.product.id === product.id
                        ? { ...item, quantity: item.quantity + 1 }
                        : item
                );
            }
            return [...prev, { product, quantity: 1 }];
        });
    };

    return (
        <div style={{ display: "flex", gap: "40px", padding: "20px" }}>
            <ProductCatalog onAddToCart={handleAddToCart} />
            <ShoppingCart items={cart} />
        </div>
    );
}

שימו לב שהסטייט חי ב-App ומועבר למטה. ProductCatalog מקבלת callback ו-ShoppingCart מקבלת את הנתונים.

פתרון תרגיל 7

import { useState } from "react";

interface ColorPickerProps {
    color: string;
    onChange: (color: string) => void;
}

function ColorPicker({ color, onChange }: ColorPickerProps) {
    return (
        <div>
            <h3>Pick a color:</h3>
            <input
                type="color"
                value={color}
                onChange={(e) => onChange(e.target.value)}
            />
        </div>
    );
}

interface PreviewProps {
    color: string;
}

function Preview({ color }: PreviewProps) {
    return (
        <div>
            <h3>Preview:</h3>
            <div
                style={{
                    width: "100px",
                    height: "100px",
                    backgroundColor: color,
                    borderRadius: "8px",
                    border: "1px solid #ccc",
                }}
            />
            <p>Color code: {color}</p>
        </div>
    );
}

function App() {
    const [color, setColor] = useState("#3498db");

    return (
        <div style={{ display: "flex", gap: "40px", padding: "20px" }}>
            <ColorPicker color={color} onChange={setColor} />
            <Preview color={color} />
        </div>
    );
}

תשובות לשאלות

  1. Props הם נתונים שמגיעים מבחוץ (מקומפוננטת אב) והם read-only. State הם נתונים פנימיים שהקומפוננטה מנהלת ויכולה לשנות. שינוי state גורם לרינדור מחדש. Props דומים לפרמטרים של פונקציה, state דומה למשתנים מקומיים ששומרים ערך בין קריאות.

  2. ריאקט משווה references (לא ערכים עמוקים) כדי לדעת אם הסטייט השתנה. אם משנים אובייקט קיים ומעבירים את אותה reference, ריאקט חושבת שלא השתנה כלום ולא מרנדרת מחדש. בנוסף, mutation ישירה עוקפת את מנגנון הרינדור של ריאקט ויכולה ליצור באגים קשים לאיתור.

  3. כשקוראים ל-setState: ריאקט שומרת את הערך החדש, מתזמנת רינדור מחדש, קוראת שוב לפונקציית הקומפוננטה, הפונקציה מחזירה JSX חדש, ריאקט משווה אותו ל-JSX הקודם (diffing), ומעדכנת רק את החלקים שהשתנו ב-DOM האמיתי.

  4. עדכון פונקציונלי (prev => newValue) נחוץ כשהערך החדש תלוי בערך הקודם, במיוחד כשקוראים ל-setter כמה פעמים ברצף. עדכון רגיל מספיק כשהערך החדש לא תלוי בקודם (למשל setName("Alice")).

  5. מרימים סטייט כששתי קומפוננטות או יותר צריכות לשתף את אותו מידע. הכלל: הסטייט חי באב המשותף הקרוב ביותר. הנתונים יורדים כ-props, ושינויים עולים דרך callbacks.

  6. push משנה את המערך המקורי (mutation) ולא יוצרת מערך חדש. כש-React משווה references, היא רואה אותו מערך ולא מרנדרת מחדש. צריך ליצור מערך חדש עם spread ([...arr, newItem]) או עם map/filter.