לדלג לתוכן

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>
    );
}

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

  1. React.ReactNode כולל כל דבר שאפשר לרנדר: מחרוזות, מספרים, בוליאנים, null, undefined, אלמנטי JSX, ומערכים. React.ReactElement כולל רק אלמנטי JSX (תוצאה של <Component /> או React.createElement). ברוב המקרים נעדיף ReactNode כי הוא גמיש יותר.

  2. הטיפוס הגנרי (כמו HTMLButtonElement) נותן גישה מטויפסת ל-properties הספציפיים של האלמנט. למשל, React.ChangeEvent<HTMLInputElement> מאפשר גישה ל-e.target.value עם טיפוס string, בעוד ש-HTMLSelectElement נותן גישה ל-e.target.selectedOptions.

  3. קומפוננטה גנרית מתאימה כשרוצים קומפוננטה שעובדת עם כל טיפוס נתונים - רשימות, dropdowns, טבלאות. קומפוננטה רגילה מתאימה כשה-props ידועים מראש ולא צריכים גמישות. אם הקומפוננטה מציגה תמיד אותם שדות, אין צורך בגנריקס.

  4. הרחבת React.InputHTMLAttributes נותנת אוטומטית תמיכה בכל אטריביוטי HTML של input (type, placeholder, disabled, required, maxLength ועוד עשרות). הגדרה ידנית דורשת לכתוב כל prop, מה שמוביל להרבה קוד חזרתי ותמיד חסר משהו.