מה באמת הבעיה בדוגמת ריאקט הזו

20/11/2024

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

https://matanbobi.dev/posts/stop-passing-setter-functions-to-components

הקוד שהוא הדביק נראה ככה:

// Form.jsx 
function Form() { 
    const [formData, setFormData] = useState({ name: '' }); 
    return (
        <div>
            <h1>Form</h1> 
            {/* Pass the setter function down to ChildComponent */}
            <Input name={formData.name} setFormData={setFormData} />
            <button onClick={() => console.log(formData)}>Submit</button> 
        </div> 
    ); 
};

// Input.jsx
function Input({ name, setFormData }) { 
    const handleInputChange = (event) => { 
        // Directly using the setFormData setter function from the parent
        setFormData((prevData) => ({ ...prevData, name: event.target.value })); 
    }; 

    return ( 
        <div>
            <label> 
                Name: 
                <input type="text" value={name} onChange={handleInputChange} /> 
            </label>
        </div> 
    ); 
};

הקומפוננטה Input מקבלת את setFormData מ Form ומפעילה אותה כשהטקסט משתנה. עכשיו לפני שנגיע ל"מה אם" אנחנו כבר רואים שלקומפוננטה Input יש בעיה - הפונקציה שהיא קיבלה מגיעה עם חתימה שלא מתאימה לה. קומפוננטת Input רוצה לדווח שהיה שינוי בשדה שלה, אבל צריכה לדווח את זה בצורה לא טבעית:

setFormData((prevData) => ({ ...prevData, name: event.target.value })); 

הטענה של מתן בפוסט היתה שארכיטקטורה זו היא בעייתית כי אם מחר יהיה שינוי באיך ש Form שומר את הסטייט שלו, אז החתימה של setFormData עשויה להשתנות ואז נצטרך לשנות גם את Input. המסקנה היא שלא כדאי להעביר setters שמתקבלים מ useState לקומפוננטות פנימיות.

אומנם אהבתי את הדוגמה אבל אני לא בטוח שאני מסכים עם המסקנה. נדמיין שהטופס היה בנוי כך:

// Form.jsx 
function Form() { 
    const [name, setName] = useState(''); 
    const [favoriteColor, setFavoriteColor] = useState('#000000);

    return (
        <div>
            <h1>Form</h1> 
            <Input label="Name" value={name} setter={setName} />
            <Input label="Favorite Color" value={favoriteColor} setter={setFavoriteColor} />
        </div> 
    ); 
};

// Input.jsx
function Input({ label, value, setter }) { 
    const handleInputChange = (event) => { 
        setter(event.target.value);
    }; 

    return ( 
        <div>
            <label> 
                {label}: 
                <input type="text" value={value} onChange={handleInputChange} /> 
            </label>
        </div> 
    ); 
};

במצב כזה עדיין העברתי את ה setters פנימה לקומפוננטת ה Input אבל אם יהיה שינוי באיך ש Form שומר את המידע לא תהיה בעיה לשנות את הקוד רק בקומפוננטת Form - למשל ליצור פונקציה חדשה שמקבלת את הערך החדש ולהעביר אותה פנימה ל Input. כלומר מה שהיה חשוב בדינמיקה בין שתי הקומפוננטות הוא הממשק ביניהן.

פה אפשר להוסיף התלבטות שניה והיא האם עדיף לשמור את כל אוביקט הטופס בתור State יחיד או לשבור אותו למספר משתנים פשוטים. בדוגמה אנחנו רואים ששימוש במספר משתנים פשוטים מעודד אותנו להעביר פחות מידע לקומפוננטות ולכן מבחינת ממשק אני ממליץ על שיטה זו.