6.3 טיפוסים בריאקט פתרון
פתרון - טיפוסים בריאקט¶
פתרון תרגיל 1¶
interface Column {
header: string;
accessor: string;
}
interface DataTableProps {
title: string;
columns: Column[];
data: Record<string, string | number>[];
striped?: boolean;
}
function DataTable({ title, columns, data, striped = false }: DataTableProps) {
return (
<div>
<h2>{title}</h2>
<table style={{ borderCollapse: "collapse", width: "100%" }}>
<thead>
<tr>
{columns.map((col) => (
<th key={col.accessor} style={{ border: "1px solid #ddd", padding: "8px", textAlign: "left" }}>
{col.header}
</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, rowIndex) => (
<tr
key={rowIndex}
style={{ backgroundColor: striped && rowIndex % 2 === 1 ? "#f9f9f9" : "white" }}
>
{columns.map((col) => (
<td key={col.accessor} style={{ border: "1px solid #ddd", padding: "8px" }}>
{row[col.accessor]}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}
// usage
function App() {
const columns: Column[] = [
{ header: "Name", accessor: "name" },
{ header: "Age", accessor: "age" },
{ header: "City", accessor: "city" },
];
const data = [
{ name: "Alice", age: 30, city: "Tel Aviv" },
{ name: "Bob", age: 25, city: "Haifa" },
{ name: "Charlie", age: 35, city: "Jerusalem" },
];
return <DataTable title="Users" columns={columns} data={data} striped />;
}
פתרון תרגיל 2¶
import { useState } from "react";
function InteractiveBox() {
const [info, setInfo] = useState("");
const [bgColor, setBgColor] = useState("#f0f0f0");
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
setInfo(`Clicked at (${e.clientX}, ${e.clientY})`);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInfo(`Typed: ${e.target.value}`);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setInfo("Form submitted!");
};
const handleMouseEnter = (e: React.MouseEvent<HTMLDivElement>) => {
setBgColor("#d0e8ff");
};
const handleMouseLeave = (e: React.MouseEvent<HTMLDivElement>) => {
setBgColor("#f0f0f0");
};
return (
<div
onClick={handleClick}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
style={{ backgroundColor: bgColor, padding: "20px", borderRadius: "8px" }}
>
<form onSubmit={handleSubmit}>
<input type="text" onChange={handleChange} placeholder="Type something..." />
<button type="submit">Submit</button>
</form>
<p>{info}</p>
</div>
);
}
פתרון תרגיל 3¶
interface ThemeBoxProps {
theme: "light" | "dark" | "colorful";
customStyle?: React.CSSProperties;
children: React.ReactNode;
}
const themeStyles: Record<string, React.CSSProperties> = {
light: {
backgroundColor: "#ffffff",
color: "#333333",
border: "1px solid #e0e0e0",
},
dark: {
backgroundColor: "#1a1a2e",
color: "#eaeaea",
border: "1px solid #333",
},
colorful: {
backgroundColor: "#ff6b6b",
color: "#ffffff",
border: "2px solid #ee5a24",
},
};
function ThemeBox({ theme, customStyle, children }: ThemeBoxProps) {
const baseStyle: React.CSSProperties = {
padding: "20px",
borderRadius: "8px",
...themeStyles[theme],
...customStyle,
};
return <div style={baseStyle}>{children}</div>;
}
// usage
function App() {
return (
<div style={{ display: "flex", gap: "16px", padding: "20px" }}>
<ThemeBox theme="light">Light theme</ThemeBox>
<ThemeBox theme="dark">Dark theme</ThemeBox>
<ThemeBox theme="colorful" customStyle={{ fontSize: "20px" }}>
Colorful theme
</ThemeBox>
</div>
);
}
פתרון תרגיל 4¶
interface DropdownProps<T> {
options: T[];
getLabel: (option: T) => string;
getValue: (option: T) => string;
selected: T | null;
onChange: (option: T) => void;
}
function Dropdown<T>({ options, getLabel, getValue, selected, onChange }: DropdownProps<T>) {
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const found = options.find((opt) => getValue(opt) === e.target.value);
if (found) onChange(found);
};
return (
<select
value={selected ? getValue(selected) : ""}
onChange={handleChange}
>
<option value="" disabled>Select...</option>
{options.map((option) => (
<option key={getValue(option)} value={getValue(option)}>
{getLabel(option)}
</option>
))}
</select>
);
}
// usage with strings
function StringDropdown() {
const [selected, setSelected] = useState<string | null>(null);
const fruits = ["Apple", "Banana", "Cherry"];
return (
<Dropdown
options={fruits}
getLabel={(f) => f}
getValue={(f) => f}
selected={selected}
onChange={setSelected}
/>
);
}
// usage with objects
interface Product {
id: string;
name: string;
price: number;
}
function ProductDropdown() {
const [selected, setSelected] = useState<Product | null>(null);
const products: Product[] = [
{ id: "1", name: "Keyboard", price: 79.99 },
{ id: "2", name: "Mouse", price: 29.99 },
];
return (
<div>
<Dropdown<Product>
options={products}
getLabel={(p) => `${p.name} - $${p.price}`}
getValue={(p) => p.id}
selected={selected}
onChange={setSelected}
/>
{selected && <p>Selected: {selected.name} (${selected.price})</p>}
</div>
);
}
פתרון תרגיל 5¶
interface TextInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
error?: string;
helperText?: string;
}
function TextInput({ label, error, helperText, ...rest }: TextInputProps) {
return (
<div style={{ marginBottom: "16px" }}>
<label style={{ display: "block", marginBottom: "4px", fontWeight: "bold" }}>
{label}
</label>
<input
{...rest}
style={{
width: "100%",
padding: "8px",
border: `1px solid ${error ? "red" : "#ccc"}`,
borderRadius: "4px",
boxSizing: "border-box",
}}
/>
{error && <p style={{ color: "red", fontSize: "12px", margin: "4px 0 0" }}>{error}</p>}
{!error && helperText && (
<p style={{ color: "#666", fontSize: "12px", margin: "4px 0 0" }}>{helperText}</p>
)}
</div>
);
}
// usage - all standard HTML attributes work
function App() {
return (
<form>
<TextInput
label="Username"
placeholder="Enter username"
required
maxLength={20}
helperText="Maximum 20 characters"
/>
<TextInput
label="Email"
type="email"
placeholder="Enter email"
error="Invalid email address"
/>
<TextInput
label="Password"
type="password"
placeholder="Enter password"
minLength={8}
/>
</form>
);
}
פתרון תרגיל 6¶
import { useState } from "react";
interface User {
id: number;
name: string;
avatar: string;
}
interface Message {
id: number;
text: string;
sender: User;
timestamp: Date;
type: "text" | "image" | "system";
}
interface ChatMessageProps {
message: Message;
}
function ChatMessage({ message }: ChatMessageProps) {
if (message.type === "system") {
return (
<div style={{ textAlign: "center", color: "#888", padding: "4px", fontStyle: "italic" }}>
{message.text}
</div>
);
}
return (
<div style={{ display: "flex", gap: "8px", padding: "8px" }}>
<img
src={message.sender.avatar}
alt={message.sender.name}
style={{ width: "36px", height: "36px", borderRadius: "50%" }}
/>
<div>
<div>
<strong>{message.sender.name}</strong>
<span style={{ color: "#888", fontSize: "12px", marginLeft: "8px" }}>
{message.timestamp.toLocaleTimeString()}
</span>
</div>
<p style={{ margin: "4px 0 0" }}>{message.text}</p>
</div>
</div>
);
}
interface ChatWindowProps {
messages: Message[];
}
function ChatWindow({ messages }: ChatWindowProps) {
return (
<div style={{ border: "1px solid #ddd", borderRadius: "8px", padding: "8px", maxHeight: "400px", overflowY: "auto" }}>
{messages.map((msg) => (
<ChatMessage key={msg.id} message={msg} />
))}
</div>
);
}
interface ChatInputProps {
onSend: (text: string) => void;
}
function ChatInput({ onSend }: ChatInputProps) {
const [text, setText] = useState("");
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (text.trim()) {
onSend(text);
setText("");
}
};
return (
<form onSubmit={handleSubmit} style={{ display: "flex", gap: "8px", marginTop: "8px" }}>
<input
type="text"
value={text}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setText(e.target.value)}
placeholder="Type a message..."
style={{ flex: 1, padding: "8px" }}
/>
<button type="submit">Send</button>
</form>
);
}
תשובות לשאלות¶
-
React.ReactNodeכולל כל דבר שאפשר לרנדר: מחרוזות, מספרים, בוליאנים, null, undefined, אלמנטי JSX, ומערכים.React.ReactElementכולל רק אלמנטי JSX (תוצאה של<Component />אוReact.createElement). ברוב המקרים נעדיףReactNodeכי הוא גמיש יותר. -
הטיפוס הגנרי (כמו
HTMLButtonElement) נותן גישה מטויפסת ל-properties הספציפיים של האלמנט. למשל,React.ChangeEvent<HTMLInputElement>מאפשר גישה ל-e.target.valueעם טיפוסstring, בעוד ש-HTMLSelectElementנותן גישה ל-e.target.selectedOptions. -
קומפוננטה גנרית מתאימה כשרוצים קומפוננטה שעובדת עם כל טיפוס נתונים - רשימות, dropdowns, טבלאות. קומפוננטה רגילה מתאימה כשה-props ידועים מראש ולא צריכים גמישות. אם הקומפוננטה מציגה תמיד אותם שדות, אין צורך בגנריקס.
-
הרחבת
React.InputHTMLAttributesנותנת אוטומטית תמיכה בכל אטריביוטי HTML של input (type, placeholder, disabled, required, maxLength ועוד עשרות). הגדרה ידנית דורשת לכתוב כל prop, מה שמוביל להרבה קוד חזרתי ותמיד חסר משהו.