Props Drilling והרמת State

מבוא

ככל שאפליקציה גדלה ועץ הקומפוננטות מסתעף, צצות שתי בעיות נפוצות בניהול הנתונים:

  1. Props Drilling - הצורך להעביר props דרך רמות ביניים רבות שאינן זקוקות להם
  2. שיתוף State - מצב שבו כמה קומפוננטות צריכות לקרוא ולעדכן את אותו נתון

בשיעור זה נכיר את שתי הבעיות ואת הפתרון הבסיסי לשיתוף State - Lifting State Up.


Props Drilling - הבעיה

Props Drilling קורה כשמעבירים props דרך קומפוננטות ביניים שלא צריכות אותם:

// ❌ Props Drilling - הנתון עובר דרך 3 רמות!
function App() {
    const [user, setUser] = useState({ name: 'דני' });

    return <Layout user={user} />;  // רמה 1
}

function Layout({ user }) {
    return (
        <div>
            <Sidebar user={user} />  {/* רמה 2 - לא צריך את user */}
            <Content />
        </div>
    );
}

function Sidebar({ user }) {
    return (
        <div>
            <UserProfile user={user} />  {/* רמה 3 - לא צריך את user */}
        </div>
    );
}

function UserProfile({ user }) {
    return <p>שלום, {user.name}!</p>;  // רמה 4 - כאן באמת צריך!
}

בעיות של Props Drilling:

  1. קוד מסורבל וקשה לתחזוקה
  2. קומפוננטות ביניים "מזוהמות" ב-props לא רלוונטיים
  3. קשה לשנות את מבנה הנתונים

Lifting State Up - פתרון לשיתוף State

העיקרון של Lifting State Up ("הרמת המצב") פשוט: כאשר כמה קומפוננטות צריכות לשתף נתון, מעבירים אותו למעלה - אל קומפוננטת ההורה המשותפת הקרובה ביותר. ההורה מחזיק את ה-State ומעביר אותו לילדים כ-props, יחד עם פונקציית עדכון. כך כל הילדים עובדים על אותו מקור נתונים יחיד ונשארים מסונכרנים.

הבעיה: שתי קומפוננטות צריכות את אותו נתון

// ❌ בעיה - כל קומפוננטה עם state נפרד
function TemperatureInput() {
    const [celsius, setCelsius] = useState(0);
    return <input value={celsius} onChange={e => setCelsius(e.target.value)} />;
}

function TemperatureDisplay() {
    const [celsius, setCelsius] = useState(0); // לא מסונכרן!
    return <p>{celsius}°C = {(celsius * 9/5) + 32}°F</p>;
}

הפתרון: Lifting State Up

// ✅ פתרון - State בהורה המשותף
function TemperatureConverter() {
    // State מורם להורה
    const [celsius, setCelsius] = useState(0);

    return (
        <div>
            <TemperatureInput 
                value={celsius} 
                onChange={setCelsius} 
            />
            <TemperatureDisplay celsius={celsius} />
        </div>
    );
}

function TemperatureInput({ value, onChange }) {
    return (
        <input 
            type="number"
            value={value} 
            onChange={e => onChange(Number(e.target.value))} 
        />
    );
}

function TemperatureDisplay({ celsius }) {
    const fahrenheit = (celsius * 9/5) + 32;
    return <p>{celsius}°C = {fahrenheit}°F</p>;
}

דוגמה מלאה: טופס עם ולידציה

function RegistrationForm() {
    // State מורם - כל הנתונים בהורה
    const [formData, setFormData] = useState({
        email: '',
        password: '',
        confirmPassword: ''
    });

    const [errors, setErrors] = useState({});

    // פונקציית עדכון שמועברת לילדים
    const updateField = (field, value) => {
        setFormData(prev => ({ ...prev, [field]: value }));
    };

    // ולידציה
    const validate = () => {
        const newErrors = {};
        if (!formData.email.includes('@')) {
            newErrors.email = 'אימייל לא תקין';
        }
        if (formData.password.length < 6) {
            newErrors.password = 'סיסמה חייבת להכיל לפחות 6 תווים';
        }
        if (formData.password !== formData.confirmPassword) {
            newErrors.confirmPassword = 'הסיסמאות לא תואמות';
        }
        setErrors(newErrors);
        return Object.keys(newErrors).length === 0;
    };

    return (
        <form onSubmit={e => { e.preventDefault(); validate(); }}>
            <EmailInput 
                value={formData.email}
                onChange={v => updateField('email', v)}
                error={errors.email}
            />
            <PasswordInput 
                value={formData.password}
                onChange={v => updateField('password', v)}
                error={errors.password}
            />
            <ConfirmPasswordInput 
                value={formData.confirmPassword}
                onChange={v => updateField('confirmPassword', v)}
                error={errors.confirmPassword}
            />
            <FormSummary formData={formData} errors={errors} />
            <button type="submit">הרשם</button>
        </form>
    );
}

function EmailInput({ value, onChange, error }) {
    return (
        <div>
            <input 
                type="email"
                value={value}
                onChange={e => onChange(e.target.value)}
                placeholder="אימייל"
            />
            {error && <span className="error">{error}</span>}
        </div>
    );
}

function PasswordInput({ value, onChange, error }) {
    return (
        <div>
            <input 
                type="password"
                value={value}
                onChange={e => onChange(e.target.value)}
                placeholder="סיסמה"
            />
            {error && <span className="error">{error}</span>}
        </div>
    );
}

function ConfirmPasswordInput({ value, onChange, error }) {
    return (
        <div>
            <input 
                type="password"
                value={value}
                onChange={e => onChange(e.target.value)}
                placeholder="אימות סיסמה"
            />
            {error && <span className="error">{error}</span>}
        </div>
    );
}

function FormSummary({ formData, errors }) {
    const isValid = Object.keys(errors).length === 0 && 
                    formData.email && formData.password;

    return (
        <div className="summary">
            <p>סטטוס: {isValid ? '✅ תקין' : '❌ לא מלא'}</p>
        </div>
    );
}

דפוס: Controlled Component

קומפוננטה מבוקרת (Controlled Component) היא קומפוננטה שאינה מחזיקה State משלה, אלא מקבלת את הערך שלה מההורה ומדווחת לו על כל שינוי. זהו יישום ישיר של Lifting State Up, והדפוס המקובל לעבודה עם שדות טופס ב-React:

// ✅ Controlled Component
function TextInput({ value, onChange, label }) {
    return (
        <label>
            {label}:
            <input 
                value={value}           // ערך מההורה
                onChange={e => onChange(e.target.value)}  // עדכון להורה
            />
        </label>
    );
}

// שימוש
function Parent() {
    const [name, setName] = useState('');
    const [email, setEmail] = useState('');

    return (
        <div>
            <TextInput label="שם" value={name} onChange={setName} />
            <TextInput label="אימייל" value={email} onChange={setEmail} />
        </div>
    );
}

מתי להשתמש בכל גישה?

מצב פתרון מומלץ
2-3 קומפוננטות משתפות state Lifting State Up
Props עוברים דרך 3+ רמות Context API (שיעור 15)
State גלובלי מורכב Redux / Zustand
State מקומי בלבד useState רגיל

טעויות נפוצות

1. הרמה לא נכונה

// ❌ שגוי - State ברמה גבוהה מדי
function App() {
    const [inputValue, setInputValue] = useState(''); // לא צריך להיות כאן!
    return <DeepNestedComponent value={inputValue} />;
}

// ✅ נכון - State בהורה המשותף הקרוב ביותר
function FormSection() {
    const [inputValue, setInputValue] = useState('');
    return <SomeInput value={inputValue} onChange={setInputValue} />;
}

2. שכפול State

// ❌ שגוי - State משוכפל
function Child({ user }) {
    const [localUser, setLocalUser] = useState(user); // שכפול!
    // localUser לא יתעדכן כשהprop משתנה
}

// ✅ נכון - שימוש ישיר ב-prop
function Child({ user, onUserChange }) {
    // ללא state מקומי, עובדים ישירות עם ה-prop
}

סיכום

מושג הסבר
Props Drilling העברת props דרך קומפוננטות ביניים
Lifting State Up העלאת state להורה משותף
Controlled Component קומפוננטה ש-state שלה מנוהל בחוץ
Single Source of Truth מקור אחד לנתון

הקודם: ← Lifecycle & useEffect
הבא: Component Communication Patterns →

בדקו את עצמכם

נסו לענות לבד לפני שאתם פותחים את התשובה.

  1. מה זה Props Drilling?

    1. Hook מיוחד
    2. props עם הרבה שדות
    3. העברת props דרך הרבה רמות שלא צריכות אותם
    4. מחיקת props
    הצגת התשובה

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

  2. מה זה Lifting State Up?

    1. להעלות state לשרת
    2. להעביר state להורה המשותף של כמה קומפוננטות שצריכות אותו
    3. להפוך state ל-props
    4. למחוק state
    הצגת התשובה

    תשובה ב. כך מקור אמת אחד משותף לכל מי שצריך אותו.