לדלג לתוכן

6.7 טפסים בריאקט הרצאה

טפסים בריאקט - Forms in React

טפסים הם חלק בסיסי בכל אפליקציה - רישום, התחברות, חיפוש, הגדרות, ועוד. בריאקט, הגישה המומלצת לטפסים היא controlled components - קומפוננטות שבהן ריאקט שולטת בערכי השדות.

קומפוננטות מבוקרות - controlled components

בקומפוננטה מבוקרת, ערך השדה נשלט על ידי הסטייט של ריאקט. כל שינוי עובר דרך ה-handler ומעדכן את הסטייט:

import { useState } from "react";

function ControlledInput() {
    const [name, setName] = useState("");

    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        setName(e.target.value);
    };

    return (
        <div>
            <input type="text" value={name} onChange={handleChange} />
            <p>Hello, {name}!</p>
        </div>
    );
}

הזרימה:
1. המשתמש מקליד תו
2. onChange נורה עם הערך החדש
3. setName מעדכן את הסטייט
4. ריאקט מרנדרת מחדש - ה-input מציג את הערך המעודכן

למה controlled ולא uncontrolled

ב-HTML רגיל, ה-DOM מנהל את ערך ה-input. בגישה controlled, ריאקט היא מקור האמת היחיד (single source of truth). זה מאפשר:

  • ולידציה בזמן אמת
  • שליטה מלאה בפורמט הקלט
  • השבתה מותנית של כפתור השליחה
  • סנכרון ערכים בין כמה שדות

טיפול בסוגי שדות שונים

שדה טקסט - input text

function TextInput() {
    const [value, setValue] = useState("");

    return (
        <input
            type="text"
            value={value}
            onChange={(e) => setValue(e.target.value)}
            placeholder="Enter text"
        />
    );
}

אזור טקסט - textarea

ב-HTML, textarea משתמש בתוכן פנימי. בריאקט, הוא עובד כמו input עם value:

function TextArea() {
    const [text, setText] = useState("");

    return (
        <div>
            <textarea
                value={text}
                onChange={(e) => setText(e.target.value)}
                rows={4}
                placeholder="Write something..."
            />
            <p>Characters: {text.length}</p>
        </div>
    );
}

רשימה נפתחת - select

ב-HTML, האופציה הנבחרת מסומנת עם selected. בריאקט, משתמשים ב-value על ה-select:

function SelectInput() {
    const [color, setColor] = useState("red");

    return (
        <select value={color} onChange={(e) => setColor(e.target.value)}>
            <option value="red">Red</option>
            <option value="green">Green</option>
            <option value="blue">Blue</option>
        </select>
    );
}

תיבת סימון - checkbox

checkbox משתמש ב-checked במקום value:

function Checkbox() {
    const [isChecked, setIsChecked] = useState(false);

    return (
        <label>
            <input
                type="checkbox"
                checked={isChecked}
                onChange={(e) => setIsChecked(e.target.checked)}
            />
            I agree to the terms
        </label>
    );
}

שימו לב: קוראים מ-e.target.checked (בוליאני), לא מ-e.target.value.

כפתורי רדיו - radio buttons

קבוצת radio חולקת את אותו name, והערך הנבחר נשמר בסטייט אחד:

function RadioGroup() {
    const [size, setSize] = useState("medium");

    return (
        <div>
            <label>
                <input
                    type="radio"
                    name="size"
                    value="small"
                    checked={size === "small"}
                    onChange={(e) => setSize(e.target.value)}
                />
                Small
            </label>
            <label>
                <input
                    type="radio"
                    name="size"
                    value="medium"
                    checked={size === "medium"}
                    onChange={(e) => setSize(e.target.value)}
                />
                Medium
            </label>
            <label>
                <input
                    type="radio"
                    name="size"
                    value="large"
                    checked={size === "large"}
                    onChange={(e) => setSize(e.target.value)}
                />
                Large
            </label>
            <p>Selected: {size}</p>
        </div>
    );
}

שליחת טופס - form submission

משתמשים ב-onSubmit על אלמנט ה-form. חשוב לקרוא ל-preventDefault כדי למנוע רענון הדף:

function LoginForm() {
    const [email, setEmail] = useState("");
    const [password, setPassword] = useState("");

    const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        console.log("Login:", { email, password });
        // send to server...
    };

    return (
        <form onSubmit={handleSubmit}>
            <div>
                <label>Email:</label>
                <input
                    type="email"
                    value={email}
                    onChange={(e) => setEmail(e.target.value)}
                />
            </div>
            <div>
                <label>Password:</label>
                <input
                    type="password"
                    value={password}
                    onChange={(e) => setPassword(e.target.value)}
                />
            </div>
            <button type="submit">Login</button>
        </form>
    );
}

כמה שדות עם handler אחד

כשיש הרבה שדות, במקום handler נפרד לכל שדה, אפשר לנהל את כולם עם אובייקט סטייט אחד ו-handler אחד:

interface FormData {
    firstName: string;
    lastName: string;
    email: string;
    age: string;
}

function RegistrationForm() {
    const [form, setForm] = useState<FormData>({
        firstName: "",
        lastName: "",
        email: "",
        age: "",
    });

    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        const { name, value } = e.target;
        setForm({ ...form, [name]: value });
    };

    const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        console.log("Form data:", form);
    };

    return (
        <form onSubmit={handleSubmit}>
            <input name="firstName" value={form.firstName} onChange={handleChange} placeholder="First Name" />
            <input name="lastName" value={form.lastName} onChange={handleChange} placeholder="Last Name" />
            <input name="email" value={form.email} onChange={handleChange} placeholder="Email" type="email" />
            <input name="age" value={form.age} onChange={handleChange} placeholder="Age" type="number" />
            <button type="submit">Register</button>
        </form>
    );
}

המפתח: כל input צריך אטריביוט name שתואם לשם השדה באובייקט הסטייט. ה-handler משתמש ב-[name]: value (computed property) כדי לעדכן את השדה הנכון.

טיפול ב-checkbox וב-select עם handler אחד

כשהטופס כולל סוגי שדות שונים, ה-handler צריך לבדוק את סוג השדה:

interface FormData {
    name: string;
    email: string;
    role: string;
    newsletter: boolean;
}

function MixedForm() {
    const [form, setForm] = useState<FormData>({
        name: "",
        email: "",
        role: "user",
        newsletter: false,
    });

    const handleChange = (
        e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
    ) => {
        const target = e.target;
        let value: string | boolean;

        if (target instanceof HTMLInputElement && target.type === "checkbox") {
            value = target.checked;
        } else {
            value = target.value;
        }

        setForm({ ...form, [target.name]: value });
    };

    const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        console.log(form);
    };

    return (
        <form onSubmit={handleSubmit}>
            <input name="name" value={form.name} onChange={handleChange} placeholder="Name" />
            <input name="email" value={form.email} onChange={handleChange} placeholder="Email" type="email" />
            <select name="role" value={form.role} onChange={handleChange}>
                <option value="user">User</option>
                <option value="admin">Admin</option>
                <option value="editor">Editor</option>
            </select>
            <label>
                <input
                    name="newsletter"
                    type="checkbox"
                    checked={form.newsletter}
                    onChange={handleChange}
                />
                Subscribe to newsletter
            </label>
            <button type="submit">Submit</button>
        </form>
    );
}

ולידציה בסיסית

אפשר להוסיף ולידציה שמופעלת בזמן אמת או בעת השליחה:

interface FormData {
    email: string;
    password: string;
}

interface FormErrors {
    email?: string;
    password?: string;
}

function ValidatedForm() {
    const [form, setForm] = useState<FormData>({ email: "", password: "" });
    const [errors, setErrors] = useState<FormErrors>({});

    const validate = (): boolean => {
        const newErrors: FormErrors = {};

        if (!form.email.includes("@")) {
            newErrors.email = "Invalid email address";
        }

        if (form.password.length < 8) {
            newErrors.password = "Password must be at least 8 characters";
        }

        setErrors(newErrors);
        return Object.keys(newErrors).length === 0;
    };

    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        setForm({ ...form, [e.target.name]: e.target.value });
    };

    const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        if (validate()) {
            console.log("Form is valid:", form);
        }
    };

    return (
        <form onSubmit={handleSubmit}>
            <div>
                <input
                    name="email"
                    type="email"
                    value={form.email}
                    onChange={handleChange}
                    placeholder="Email"
                />
                {errors.email && <p style={{ color: "red" }}>{errors.email}</p>}
            </div>
            <div>
                <input
                    name="password"
                    type="password"
                    value={form.password}
                    onChange={handleChange}
                    placeholder="Password"
                />
                {errors.password && <p style={{ color: "red" }}>{errors.password}</p>}
            </div>
            <button type="submit">Submit</button>
        </form>
    );
}

איפוס טופס

לאחר שליחה מוצלחת, לרוב רוצים לאפס את הטופס:

const initialForm: FormData = {
    firstName: "",
    lastName: "",
    email: "",
    age: "",
};

function ResetForm() {
    const [form, setForm] = useState<FormData>(initialForm);

    const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        console.log("Submitted:", form);
        setForm(initialForm); // reset to initial values
    };

    // ...
}

שמירת הערכים ההתחלתיים במשתנה מחוץ לקומפוננטה (או ב-const מחוץ לפונקציה) מאפשרת איפוס קל.

קומפוננטות מבוקרות לעומת לא מבוקרות

בנוסף ל-controlled components, יש גם uncontrolled components שמשתמשים ב-ref כדי לגשת ישירות ל-DOM:

import { useRef } from "react";

function UncontrolledInput() {
    const inputRef = useRef<HTMLInputElement>(null);

    const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        console.log("Value:", inputRef.current?.value);
    };

    return (
        <form onSubmit={handleSubmit}>
            <input ref={inputRef} type="text" defaultValue="" />
            <button type="submit">Submit</button>
        </form>
    );
}

ההבדלים:

controlled uncontrolled
ערך נשלט על ידי סטייט ערך נשלט על ידי ה-DOM
value + onChange defaultValue + ref
ולידציה בזמן אמת ולידציה בשליחה בלבד
יותר קוד פחות קוד
יותר שליטה פחות שליטה

ברוב המקרים, controlled components הם הגישה המומלצת. השתמשו ב-uncontrolled רק כשאין צורך בשליטה בערך השדה (למשל, file input).

סיכום

  • קומפוננטות מבוקרות (controlled) - ריאקט שולטת בערכי השדות דרך סטייט
  • כל שדה input צריך value ו-onChange (או checked ו-onChange עבור checkbox)
  • textarea ו-select עובדים כמו input עם value בריאקט
  • שליחת טופס עם onSubmit - חשוב לקרוא ל-preventDefault
  • handler אחד לכמה שדות - שימוש ב-name ו-computed property [name]: value
  • ולידציה אפשרית בזמן אמת או בשליחה
  • שמירת ערכים התחלתיים מאפשרת איפוס קל של הטופס
  • controlled components הם הגישה המומלצת ברוב המקרים