תקשורת בין קומפוננטות
מבוא
אפליקציה אמיתית בנויה מעשרות קומפוננטות שצריכות להעביר מידע ביניהן. הדרך הנכונה לתקשר תלויה בקשר בין הקומפוננטות בעץ. אלו דפוסי התקשורת המרכזיים, מהפשוט למתקדם:
- Parent → Child: העברת נתונים כלפי מטה באמצעות Props
- Child → Parent: החזרת מידע כלפי מעלה באמצעות פונקציות Callback
- Siblings (אחים): תקשורת דרך ההורה המשותף (Lifting State Up)
- 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 →
בדקו את עצמכם
נסו לענות לבד לפני שאתם פותחים את התשובה.
-
איך ילד מעדכן משהו אצל ההורה?
הצגת התשובה
תשובה ד. נתונים יורדים כ-props, ואירועים עולים כקריאה לפונקציות.
-
מה היתרון בהרכבה (composition) עם children?
הצגת התשובה
תשובה א. Card או Modal גנריים מקבלים כל תוכן, בלי props לכל מקרה.