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>
);
}
תשובות לשאלות¶
-
Props הם נתונים שהקומפוננטה מקבלת מבחוץ ולא יכולה לשנות. State הם נתונים פנימיים של הקומפוננטה שהיא יכולה לשנות (ושינוי שלהם גורם לרינדור מחדש).
-
Props הם read-only כי ריאקט עובדת על עיקרון של one-way data flow - נתונים זורמים מלמעלה למטה. אם קומפוננטת בן הייתה יכולה לשנות props, זה היה יוצר בלבול לגבי מי "הבעלים" של הנתונים, ומקשה על מעקב אחרי שינויים.
-
childrenהוא prop מיוחד שמגיע אוטומטית - התוכן שבין תגית הפתיחה לסגירה של הקומפוננטה. Props רגילים מועברים כאטריביוטים על התגית. מבחינת טכנית children הוא פשוט עוד prop, אבל הוא מאפשר תחביר יותר טבעי ו-HTML-י. -
נשתמש ב-callback props כשקומפוננטת בן צריכה ליידע את קומפוננטת האב שמשהו קרה - למשל לחיצה על כפתור, שינוי בקלט, בחירה מרשימה. הבן לא יודע מה האב יעשה עם המידע, הוא רק קורא לפונקציה.
-
כדאי לפצל כשקומפוננטה גדלה מדי וקשה לקריאה, כשחלק מה-UI חוזר על עצמו, כשרוצים לשתף UI בין מסכים, או כשחלק מהלוגיקה הוא יחידה עצמאית. אין צורך לפצל כל דבר - מתחילים פשוט ומפצלים כשזה הופך לנחוץ.