תקשורת בין קומפוננטות

מבוא

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

  1. Parent → Child: העברת נתונים כלפי מטה באמצעות Props
  2. Child → Parent: החזרת מידע כלפי מעלה באמצעות פונקציות Callback
  3. Siblings (אחים): תקשורת דרך ההורה המשותף (Lifting State Up)
  4. Deep / Global: שיתוף רחב באמצעות Context API או ספריית ניהול State

1. Parent → Child (Props)

הדרך הפשוטה ביותר - העברת נתונים מהורה לילד:

// Parent מעביר נתונים ל-Child
function Parent() {
    const data = { name: 'דני', age: 25 };

    return <Child user={data} />;
}

function Child({ user }) {
    return <p>שלום {user.name}, בן {user.age}</p>;
}

2. Child → Parent (Callbacks)

הילד קורא לפונקציה שנשלחה מההורה:

// Parent מקבל נתונים מ-Child
function Parent() {
    const [message, setMessage] = useState('');

    const handleMessage = (msg) => {
        console.log('התקבל מהילד:', msg);
        setMessage(msg);
    };

    return (
        <div>
            <Child onSendMessage={handleMessage} />
            <p>הודעה מהילד: {message}</p>
        </div>
    );
}

function Child({ onSendMessage }) {
    return (
        <button onClick={() => onSendMessage('שלום מהילד!')}>
            שלח הודעה להורה
        </button>
    );
}

דוגמה מלאה: טופס

function ProductForm() {
    const handleSubmit = (productData) => {
        console.log('מוצר חדש:', productData);
        // שליחה לשרת וכו'
    };

    return <ProductFormInputs onSubmit={handleSubmit} />;
}

function ProductFormInputs({ onSubmit }) {
    const [name, setName] = useState('');
    const [price, setPrice] = useState('');

    const handleFormSubmit = (e) => {
        e.preventDefault();
        onSubmit({ name, price: Number(price) });
    };

    return (
        <form onSubmit={handleFormSubmit}>
            <input 
                value={name} 
                onChange={e => setName(e.target.value)}
                placeholder="שם מוצר"
            />
            <input 
                type="number"
                value={price} 
                onChange={e => setPrice(e.target.value)}
                placeholder="מחיר"
            />
            <button type="submit">הוסף מוצר</button>
        </form>
    );
}

3. Sibling Communication

קומפוננטות אחיות מתקשרות דרך ההורה המשותף:

function App() {
    const [selectedItem, setSelectedItem] = useState(null);

    return (
        <div className="app">
            {/* אח 1: בוחר פריט */}
            <ItemList 
                onSelect={setSelectedItem} 
            />

            {/* אח 2: מציג את הבחירה */}
            <ItemDetails 
                item={selectedItem} 
            />
        </div>
    );
}

function ItemList({ onSelect }) {
    const items = ['פריט א', 'פריט ב', 'פריט ג'];

    return (
        <ul>
            {items.map(item => (
                <li key={item} onClick={() => onSelect(item)}>
                    {item}
                </li>
            ))}
        </ul>
    );
}

function ItemDetails({ item }) {
    if (!item) return <p>בחר פריט מהרשימה</p>;
    return <p>נבחר: {item}</p>;
}

4. Event Emitter Pattern

דפוס מתקדם המבוסס על "אפיק אירועים" מרכזי (Event Bus): קומפוננטה אחת משדרת אירוע והאחרות מאזינות לו, בלי קשר ישיר ביניהן. ברוב המקרים Context API יהיה פשוט ונקי יותר, אך כדאי להכיר את הדפוס:

// פשוט יותר להשתמש ב-Context, אבל זה דפוס אפשרי
const eventBus = {
    events: {},

    subscribe(event, callback) {
        if (!this.events[event]) {
            this.events[event] = [];
        }
        this.events[event].push(callback);

        // Unsubscribe function
        return () => {
            this.events[event] = this.events[event].filter(cb => cb !== callback);
        };
    },

    emit(event, data) {
        if (this.events[event]) {
            this.events[event].forEach(callback => callback(data));
        }
    }
};

function ComponentA() {
    return (
        <button onClick={() => eventBus.emit('message', 'שלום!')}>
            שלח הודעה
        </button>
    );
}

function ComponentB() {
    const [message, setMessage] = useState('');

    useEffect(() => {
        const unsubscribe = eventBus.subscribe('message', setMessage);
        return unsubscribe;
    }, []);

    return <p>הודעה: {message}</p>;
}

5. Compound Components Pattern

דפוס שבו קבוצת קומפוננטות עובדת יחד כיחידה אחת וחולקת State פנימי, בעוד המשתמש מרכיב אותן בחופשיות. דוגמה קלאסית היא רכיב טאבים (Tabs ו-Tab), שמעניק API הצהרתי ונקי:

// קומפוננטות שמשתפות state באופן מובנה
function Tabs({ children, defaultTab = 0 }) {
    const [activeTab, setActiveTab] = useState(defaultTab);

    return (
        <div className="tabs">
            <div className="tab-list">
                {React.Children.map(children, (child, index) => (
                    <button 
                        className={activeTab === index ? 'active' : ''}
                        onClick={() => setActiveTab(index)}
                    >
                        {child.props.label}
                    </button>
                ))}
            </div>
            <div className="tab-content">
                {React.Children.toArray(children)[activeTab]}
            </div>
        </div>
    );
}

function Tab({ children, label }) {
    return <div>{children}</div>;
}

// שימוש
function App() {
    return (
        <Tabs defaultTab={0}>
            <Tab label="פרופיל">
                <h2>פרופיל משתמש</h2>
                <p>תוכן הפרופיל...</p>
            </Tab>
            <Tab label="הגדרות">
                <h2>הגדרות</h2>
                <p>תוכן ההגדרות...</p>
            </Tab>
            <Tab label="התראות">
                <h2>התראות</h2>
                <p>תוכן ההתראות...</p>
            </Tab>
        </Tabs>
    );
}

6. Render Props Pattern

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

// קומפוננטה שחושפת state דרך render prop
function MouseTracker({ render }) {
    const [position, setPosition] = useState({ x: 0, y: 0 });

    useEffect(() => {
        const handleMouseMove = (e) => {
            setPosition({ x: e.clientX, y: e.clientY });
        };
        window.addEventListener('mousemove', handleMouseMove);
        return () => window.removeEventListener('mousemove', handleMouseMove);
    }, []);

    // קוראים לפונקציה שהתקבלה עם ה-state
    return render(position);
}

// שימוש
function App() {
    return (
        <MouseTracker 
            render={({ x, y }) => (
                <div>
                    <p>מיקום העכבר: {x}, {y}</p>
                    <div 
                        style={{
                            position: 'absolute',
                            left: x,
                            top: y,
                            width: 20,
                            height: 20,
                            background: 'red',
                            borderRadius: '50%'
                        }}
                    />
                </div>
            )}
        />
    );
}

השוואה בין דפוסים

דפוס שימוש יתרונות חסרונות
Props Parent → Child פשוט, ברור Props drilling
Callbacks Child → Parent גמיש יכול להסתבך
Lifting State Siblings מקור אחד לאמת קוד בהורה
Context Deep/Global ללא drilling Rerenders
Compound קומפוננטות קשורות API נקי מורכב
Render Props לוגיקה משותפת גמיש מאוד Callback hell

סיכום

// Parent → Child
<Child data={data} />

// Child → Parent
<Child onChange={(value) => setParentState(value)} />

// Siblings (דרך הורה)
<Brother1 onSelect={setShared} />
<Brother2 selected={shared} />

הקודם: ← Props Drilling
הבא: Context API →

בדקו את עצמכם

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

  1. איך ילד מעדכן משהו אצל ההורה?

    1. עם window
    2. אי אפשר
    3. משנה את ה-props
    4. ההורה מעביר לו פונקציה כ-prop, והילד קורא לה
    הצגת התשובה

    תשובה ד. נתונים יורדים כ-props, ואירועים עולים כקריאה לפונקציות.

  2. מה היתרון בהרכבה (composition) עם children?

    1. קומפוננטה גמישה שלא צריכה לדעת מה יהיה בתוכה
    2. מהירות
    3. פחות קבצים
    4. אין
    הצגת התשובה

    תשובה א. Card או Modal גנריים מקבלים כל תוכן, בלי props לכל מקרה.