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>
);
}
תשובות לשאלות¶
-
onClick={handleClick}מעביר את הפונקציה כ-reference - היא תיקרא כשהמשתמש ילחץ.onClick={handleClick()}קורא לפונקציה מיד בזמן הרינדור, ומעביר את ערך ההחזרה שלה כ-handler. ברוב המקרים זה באג. -
SyntheticEvent הוא אובייקט אירוע של ריאקט שעוטף את אירוע הדפדפן המקורי. הוא מספק ממשק אחיד בין דפדפנים ומאפשר לריאקט לנהל אירועים ביעילות. אפשר לגשת לאירוע המקורי דרך
e.nativeEvent. -
&&מתאים כשרוצים להציג משהו רק אם תנאי מתקיים (אין else). ternary מתאים כשיש שתי אפשרויות - להציג דבר אחד או אחר. לתנאים מורכבים עם יותר משתי אפשרויות, עדיף if/else עם משתנה JSX או early return. -
כשמשתמשים ב-
&&עם מספר, הערך 0 הוא falsy אבל ריאקט מרנדרת אותו כטקסט.0 && <Component />מחזיר0, ו-"0" מופיע על המסך. הפתרון: תמיד להשתמש בהשוואה מפורשת (> 0) או להמיר לבוליאני. -
early return מבטל את הצורך בקינון - הקוד שאחרי ה-return יודע שהתנאי לא מתקיים. ternary מקונן (
a ? b : c ? d : e) קשה לקריאה. early return שומר על רמת קינון אחת ועושה את הקוד קריא יותר.