לדלג לתוכן

6.2 קומפוננטות ופרופס פתרון

פתרון - קומפוננטות ופרופס

פתרון תרגיל 1

interface UserCardProps {
    name: string;
    email: string;
    role: string;
}

function UserCard({ name, email, role }: UserCardProps) {
    return (
        <div style={{ border: "1px solid #ddd", padding: "16px", borderRadius: "8px", margin: "8px" }}>
            <h2>{name}</h2>
            <p>Email: {email}</p>
            <p>Role: {role}</p>
        </div>
    );
}

function App() {
    return (
        <div>
            <UserCard name="Alice" email="alice@example.com" role="Developer" />
            <UserCard name="Bob" email="bob@example.com" role="Designer" />
            <UserCard name="Charlie" email="charlie@example.com" role="Manager" />
        </div>
    );
}

פתרון תרגיל 2

interface ButtonProps {
    label: string;
    variant?: "primary" | "secondary" | "danger";
    disabled?: boolean;
    onClick: () => void;
}

function Button({ label, variant = "primary", disabled = false, onClick }: ButtonProps) {
    const colors: Record<string, string> = {
        primary: "#2563eb",
        secondary: "#6b7280",
        danger: "#dc2626",
    };

    return (
        <button
            onClick={onClick}
            disabled={disabled}
            style={{
                backgroundColor: disabled ? "#ccc" : colors[variant],
                color: "white",
                padding: "8px 16px",
                border: "none",
                borderRadius: "4px",
                cursor: disabled ? "not-allowed" : "pointer",
                fontSize: "14px",
            }}
        >
            {label}
        </button>
    );
}

function App() {
    return (
        <div style={{ display: "flex", gap: "8px", padding: "20px" }}>
            <Button label="Save" onClick={() => console.log("saved")} />
            <Button label="Cancel" variant="secondary" onClick={() => console.log("cancelled")} />
            <Button label="Delete" variant="danger" onClick={() => console.log("deleted")} />
            <Button label="Disabled" disabled onClick={() => console.log("won't fire")} />
        </div>
    );
}

פתרון תרגיל 3

interface CardProps {
    title: string;
    footer?: string;
    children: React.ReactNode;
}

function Card({ title, footer, children }: CardProps) {
    return (
        <div style={{ border: "1px solid #e5e7eb", borderRadius: "8px", overflow: "hidden" }}>
            <div style={{ backgroundColor: "#f9fafb", padding: "12px 16px", borderBottom: "1px solid #e5e7eb" }}>
                <h2 style={{ margin: 0 }}>{title}</h2>
            </div>
            <div style={{ padding: "16px" }}>{children}</div>
            {footer && (
                <div style={{ backgroundColor: "#f9fafb", padding: "12px 16px", borderTop: "1px solid #e5e7eb" }}>
                    <small>{footer}</small>
                </div>
            )}
        </div>
    );
}

function App() {
    return (
        <div style={{ display: "flex", flexDirection: "column", gap: "16px", padding: "20px" }}>
            <Card title="About me">
                <p>I am a developer.</p>
                <p>I love React.</p>
            </Card>

            <Card title="Contact" footer="Last updated: today">
                <a href="mailto:a@b.com">Email me</a>
            </Card>
        </div>
    );
}

שימו לב לשימוש ב-{footer && (...)} - זה רינדור מותנה שנלמד בהרחבה בשיעור 6.5.

פתרון תרגיל 4

interface ProductItemProps {
    name: string;
    price: number;
    onAddToCart: (productName: string) => void;
}

function ProductItem({ name, price, onAddToCart }: ProductItemProps) {
    return (
        <div style={{ border: "1px solid #ddd", padding: "12px", borderRadius: "8px", margin: "8px 0" }}>
            <h3>{name}</h3>
            <p>${price.toFixed(2)}</p>
            <button onClick={() => onAddToCart(name)}>Add to Cart</button>
        </div>
    );
}

function ProductList() {
    const handleAddToCart = (productName: string) => {
        console.log(`Added ${productName} to cart`);
    };

    return (
        <div>
            <h2>Products</h2>
            <ProductItem name="Keyboard" price={79.99} onAddToCart={handleAddToCart} />
            <ProductItem name="Mouse" price={29.99} onAddToCart={handleAddToCart} />
            <ProductItem name="Monitor" price={399.99} onAddToCart={handleAddToCart} />
        </div>
    );
}

שימו לב שה-callback מועבר מ-ProductList ל-ProductItem. כשהמשתמש לוחץ על הכפתור, ProductItem קורא ל-callback עם שם המוצר, ו-ProductList מטפל בלוגיקה.

פתרון תרגיל 5

interface AvatarProps {
    src: string;
    size?: number;
}

function Avatar({ src, size = 80 }: AvatarProps) {
    return (
        <img
            src={src}
            alt="avatar"
            style={{
                width: `${size}px`,
                height: `${size}px`,
                borderRadius: "50%",
                objectFit: "cover",
            }}
        />
    );
}

interface BadgeProps {
    text: string;
    color: string;
}

function Badge({ text, color }: BadgeProps) {
    return (
        <span
            style={{
                backgroundColor: color,
                color: "white",
                padding: "2px 8px",
                borderRadius: "12px",
                fontSize: "12px",
                marginRight: "4px",
            }}
        >
            {text}
        </span>
    );
}

interface ProfileHeaderProps {
    name: string;
    title: string;
    avatarUrl: string;
    badges: { text: string; color: string }[];
}

function ProfileHeader({ name, title, avatarUrl, badges }: ProfileHeaderProps) {
    return (
        <div style={{ display: "flex", alignItems: "center", gap: "16px", padding: "20px" }}>
            <Avatar src={avatarUrl} size={100} />
            <div>
                <h1 style={{ margin: 0 }}>{name}</h1>
                <p style={{ color: "#666" }}>{title}</p>
                <div>
                    {badges.map((badge, index) => (
                        <Badge key={index} text={badge.text} color={badge.color} />
                    ))}
                </div>
            </div>
        </div>
    );
}

function ProfilePage() {
    return (
        <div>
            <ProfileHeader
                name="Alice Johnson"
                title="Senior Developer"
                avatarUrl="https://via.placeholder.com/100"
                badges={[
                    { text: "React", color: "#61dafb" },
                    { text: "TypeScript", color: "#3178c6" },
                    { text: "Node.js", color: "#339933" },
                ]}
            />
            <div style={{ padding: "20px" }}>
                <h2>About</h2>
                <p>Full-stack developer with 5 years of experience.</p>
            </div>
        </div>
    );
}

פתרון תרגיל 6

interface LayoutProps {
    header: React.ReactNode;
    sidebar?: React.ReactNode;
    children: React.ReactNode;
    footer?: React.ReactNode;
}

function Layout({ header, sidebar, children, footer }: LayoutProps) {
    return (
        <div style={{ display: "flex", flexDirection: "column", minHeight: "100vh" }}>
            <header style={{ backgroundColor: "#1e293b", color: "white", padding: "16px" }}>
                {header}
            </header>
            <div style={{ display: "flex", flex: 1 }}>
                {sidebar && (
                    <aside style={{ width: "200px", backgroundColor: "#f1f5f9", padding: "16px" }}>
                        {sidebar}
                    </aside>
                )}
                <main style={{ flex: 1, padding: "16px" }}>{children}</main>
            </div>
            {footer && (
                <footer style={{ backgroundColor: "#1e293b", color: "white", padding: "16px" }}>
                    {footer}
                </footer>
            )}
        </div>
    );
}

function App() {
    return (
        <Layout
            header={<h1 style={{ margin: 0 }}>My Website</h1>}
            sidebar={
                <nav>
                    <ul style={{ listStyle: "none", padding: 0 }}>
                        <li><a href="#">Home</a></li>
                        <li><a href="#">About</a></li>
                        <li><a href="#">Contact</a></li>
                    </ul>
                </nav>
            }
            footer={<p style={{ margin: 0 }}>Copyright 2026</p>}
        >
            <h2>Welcome</h2>
            <p>Main content goes here.</p>
        </Layout>
    );
}

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

  1. Props הם נתונים שהקומפוננטה מקבלת מבחוץ ולא יכולה לשנות. State הם נתונים פנימיים של הקומפוננטה שהיא יכולה לשנות (ושינוי שלהם גורם לרינדור מחדש).

  2. Props הם read-only כי ריאקט עובדת על עיקרון של one-way data flow - נתונים זורמים מלמעלה למטה. אם קומפוננטת בן הייתה יכולה לשנות props, זה היה יוצר בלבול לגבי מי "הבעלים" של הנתונים, ומקשה על מעקב אחרי שינויים.

  3. children הוא prop מיוחד שמגיע אוטומטית - התוכן שבין תגית הפתיחה לסגירה של הקומפוננטה. Props רגילים מועברים כאטריביוטים על התגית. מבחינת טכנית children הוא פשוט עוד prop, אבל הוא מאפשר תחביר יותר טבעי ו-HTML-י.

  4. נשתמש ב-callback props כשקומפוננטת בן צריכה ליידע את קומפוננטת האב שמשהו קרה - למשל לחיצה על כפתור, שינוי בקלט, בחירה מרשימה. הבן לא יודע מה האב יעשה עם המידע, הוא רק קורא לפונקציה.

  5. כדאי לפצל כשקומפוננטה גדלה מדי וקשה לקריאה, כשחלק מה-UI חוזר על עצמו, כשרוצים לשתף UI בין מסכים, או כשחלק מהלוגיקה הוא יחידה עצמאית. אין צורך לפצל כל דבר - מתחילים פשוט ומפצלים כשזה הופך לנחוץ.