לדלג לתוכן

6.5 אירועים ורינדור מותנה פתרון

פתרון - אירועים ורינדור מותנה

פתרון תרגיל 1

import { useState } from "react";

function ToggleMessage() {
    const [isVisible, setIsVisible] = useState(false);

    return (
        <div>
            <button onClick={() => setIsVisible(!isVisible)}>
                {isVisible ? "Hide message" : "Show message"}
            </button>
            {isVisible && (
                <p
                    style={{
                        backgroundColor: "#e8f5e9",
                        padding: "12px",
                        borderRadius: "4px",
                        marginTop: "8px",
                    }}
                >
                    Hello! This is a toggled message.
                </p>
            )}
        </div>
    );
}

פתרון תרגיל 2

import { useState } from "react";

type AlertType = "info" | "warning" | "error";

function AlertSystem() {
    const [alert, setAlert] = useState<AlertType | null>(null);

    const colors: Record<AlertType, string> = {
        info: "#2196f3",
        warning: "#ff9800",
        error: "#f44336",
    };

    const messages: Record<AlertType, string> = {
        info: "This is an informational message.",
        warning: "Warning: please be careful!",
        error: "Error: something went wrong!",
    };

    return (
        <div>
            <div style={{ display: "flex", gap: "8px", marginBottom: "16px" }}>
                <button onClick={() => setAlert("info")}>Info</button>
                <button onClick={() => setAlert("warning")}>Warning</button>
                <button onClick={() => setAlert("error")}>Error</button>
            </div>

            {alert ? (
                <div
                    style={{
                        backgroundColor: colors[alert],
                        color: "white",
                        padding: "16px",
                        borderRadius: "4px",
                        display: "flex",
                        justifyContent: "space-between",
                        alignItems: "center",
                    }}
                >
                    <span>{messages[alert]}</span>
                    <button onClick={() => setAlert(null)}>Close</button>
                </div>
            ) : (
                <p style={{ color: "#888" }}>No active alerts.</p>
            )}
        </div>
    );
}

פתרון תרגיל 3

הבאג: כש-messages הוא מערך ריק, messages.length הוא 0. הביטוי 0 && <ul>...</ul> מחזיר 0, וריאקט מרנדרת את המספר 0 על המסך.

שלוש דרכים לתקן:

// Fix 1: explicit comparison
{messages.length > 0 && (
    <ul>{messages.map((msg, i) => <li key={i}>{msg}</li>)}</ul>
)}

// Fix 2: convert to boolean
{Boolean(messages.length) && (
    <ul>{messages.map((msg, i) => <li key={i}>{msg}</li>)}</ul>
)}

// Fix 3: ternary
{messages.length ? (
    <ul>{messages.map((msg, i) => <li key={i}>{msg}</li>)}</ul>
) : null}

הדרך הכי קריאה היא הראשונה - messages.length > 0.

פתרון תרגיל 4

import { useState } from "react";

interface Tab {
    title: string;
    content: React.ReactNode;
}

function Tabs() {
    const tabs: Tab[] = [
        { title: "About", content: <p>This is the about section. We build great software.</p> },
        { title: "Services", content: <p>We offer web development, mobile apps, and consulting.</p> },
        { title: "Contact", content: <p>Email us at hello@example.com or call 123-456.</p> },
    ];

    const [activeIndex, setActiveIndex] = useState(0);

    return (
        <div>
            <div style={{ display: "flex", borderBottom: "2px solid #ddd" }}>
                {tabs.map((tab, index) => (
                    <button
                        key={index}
                        onClick={() => setActiveIndex(index)}
                        style={{
                            padding: "10px 20px",
                            border: "none",
                            borderBottom: index === activeIndex ? "2px solid #2196f3" : "2px solid transparent",
                            backgroundColor: "transparent",
                            color: index === activeIndex ? "#2196f3" : "#666",
                            fontWeight: index === activeIndex ? "bold" : "normal",
                            cursor: "pointer",
                        }}
                    >
                        {tab.title}
                    </button>
                ))}
            </div>
            <div style={{ padding: "16px" }}>
                {tabs[activeIndex].content}
            </div>
        </div>
    );
}

פתרון תרגיל 5

import { useState } from "react";

function LoginForm() {
    const [email, setEmail] = useState("");
    const [password, setPassword] = useState("");
    const [emailTouched, setEmailTouched] = useState(false);
    const [passwordTouched, setPasswordTouched] = useState(false);
    const [isLoggedIn, setIsLoggedIn] = useState(false);

    const emailError = emailTouched && !email.includes("@") ? "Email must contain @" : "";
    const passwordError =
        passwordTouched && password.length < 6 ? "Password must be at least 6 characters" : "";

    const isValid = email.includes("@") && password.length >= 6;

    if (isLoggedIn) {
        return (
            <div style={{ color: "green", padding: "20px" }}>
                <h2>Login successful!</h2>
                <p>Welcome, {email}</p>
            </div>
        );
    }

    const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        if (isValid) {
            setIsLoggedIn(true);
        }
    };

    return (
        <form onSubmit={handleSubmit}>
            <div style={{ marginBottom: "12px" }}>
                <label>
                    Email:
                    <input
                        type="text"
                        value={email}
                        onChange={(e) => setEmail(e.target.value)}
                        onBlur={() => setEmailTouched(true)}
                        style={{ marginLeft: "8px" }}
                    />
                </label>
                {emailError && <p style={{ color: "red", fontSize: "12px" }}>{emailError}</p>}
            </div>
            <div style={{ marginBottom: "12px" }}>
                <label>
                    Password:
                    <input
                        type="password"
                        value={password}
                        onChange={(e) => setPassword(e.target.value)}
                        onBlur={() => setPasswordTouched(true)}
                        style={{ marginLeft: "8px" }}
                    />
                </label>
                {passwordError && <p style={{ color: "red", fontSize: "12px" }}>{passwordError}</p>}
            </div>
            <button type="submit" disabled={!isValid}>
                Login
            </button>
        </form>
    );
}

שימו לב לשימוש ב-onBlur כדי לסמן שהמשתמש ביקר בשדה, ול-early return למצב ההתחברות.

פתרון תרגיל 6

import { useState } from "react";

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

const allProducts: Product[] = [
    { id: 1, name: "Laptop", price: 999, category: "Electronics" },
    { id: 2, name: "Headphones", price: 79, category: "Electronics" },
    { id: 3, name: "Coffee Maker", price: 49, category: "Kitchen" },
    { id: 4, name: "Desk Lamp", price: 35, category: "Home" },
    { id: 5, name: "Keyboard", price: 129, category: "Electronics" },
    { id: 6, name: "Blender", price: 59, category: "Kitchen" },
];

type SortField = "name" | "price";

function FilterableList() {
    const [search, setSearch] = useState("");
    const [sortBy, setSortBy] = useState<SortField>("name");
    const [sortAsc, setSortAsc] = useState(true);

    const filteredProducts = allProducts
        .filter((p) => p.name.toLowerCase().includes(search.toLowerCase()))
        .sort((a, b) => {
            const modifier = sortAsc ? 1 : -1;
            if (sortBy === "name") {
                return a.name.localeCompare(b.name) * modifier;
            }
            return (a.price - b.price) * modifier;
        });

    const handleSort = (field: SortField) => {
        if (sortBy === field) {
            setSortAsc(!sortAsc);
        } else {
            setSortBy(field);
            setSortAsc(true);
        }
    };

    return (
        <div>
            <h2>Products</h2>
            <input
                type="text"
                value={search}
                onChange={(e) => setSearch(e.target.value)}
                placeholder="Search products..."
                style={{ padding: "8px", marginBottom: "12px", width: "100%" }}
            />
            <div style={{ marginBottom: "12px" }}>
                <button onClick={() => handleSort("name")}>
                    Sort by name {sortBy === "name" ? (sortAsc ? "^" : "v") : ""}
                </button>
                <button onClick={() => handleSort("price")} style={{ marginLeft: "8px" }}>
                    Sort by price {sortBy === "price" ? (sortAsc ? "^" : "v") : ""}
                </button>
            </div>
            <p>
                Showing {filteredProducts.length} of {allProducts.length} products
            </p>

            {filteredProducts.length > 0 ? (
                <ul>
                    {filteredProducts.map((product) => (
                        <li key={product.id}>
                            {product.name} - ${product.price} ({product.category})
                        </li>
                    ))}
                </ul>
            ) : (
                <p style={{ color: "#888", fontStyle: "italic" }}>No products found.</p>
            )}
        </div>
    );
}

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

  1. onClick={handleClick} מעביר את הפונקציה כ-reference - היא תיקרא כשהמשתמש ילחץ. onClick={handleClick()} קורא לפונקציה מיד בזמן הרינדור, ומעביר את ערך ההחזרה שלה כ-handler. ברוב המקרים זה באג.

  2. SyntheticEvent הוא אובייקט אירוע של ריאקט שעוטף את אירוע הדפדפן המקורי. הוא מספק ממשק אחיד בין דפדפנים ומאפשר לריאקט לנהל אירועים ביעילות. אפשר לגשת לאירוע המקורי דרך e.nativeEvent.

  3. && מתאים כשרוצים להציג משהו רק אם תנאי מתקיים (אין else). ternary מתאים כשיש שתי אפשרויות - להציג דבר אחד או אחר. לתנאים מורכבים עם יותר משתי אפשרויות, עדיף if/else עם משתנה JSX או early return.

  4. כשמשתמשים ב-&& עם מספר, הערך 0 הוא falsy אבל ריאקט מרנדרת אותו כטקסט. 0 && <Component /> מחזיר 0, ו-"0" מופיע על המסך. הפתרון: תמיד להשתמש בהשוואה מפורשת (> 0) או להמיר לבוליאני.

  5. early return מבטל את הצורך בקינון - הקוד שאחרי ה-return יודע שהתנאי לא מתקיים. ternary מקונן (a ? b : c ? d : e) קשה לקריאה. early return שומר על רמת קינון אחת ועושה את הקוד קריא יותר.