תבניות מתקדמות (HOC, Render Props, Error Boundaries)
מבוא
דפוסי עיצוב (Design Patterns) הם פתרונות מוכחים לבעיות שחוזרות שוב ושוב בפיתוח. ב-React התגבשו לאורך השנים כמה דפוסים מרכזיים לשיתוף לוגיקה ולטיפול במצבים מיוחדים. בשיעור זה נכיר את HOC, Render Props, Error Boundaries ו-Fragments, ונבין מתי כדאי להשתמש בכל אחד מהם.
HOC - Higher Order Component
HOC (רכיב מסדר גבוה) הוא פונקציה שמקבלת קומפוננטה ומחזירה קומפוננטה חדשה, "עטופה", עם יכולות נוספות. זו דרך להוסיף התנהגות משותפת (כמו בדיקת הרשאות או מצב טעינה) למספר קומפוננטות, בלי לשכתב כל אחת מהן. כיום Custom Hooks מחליפים את רוב השימושים ב-HOC, אך הדפוס עדיין נפוץ בספריות רבות:
התחביר
// HOC = פונקציה שמקבלת קומפוננטה
const withEnhancement = (WrappedComponent) => {
// ומחזירה קומפוננטה חדשה
return function EnhancedComponent(props) {
// עם לוגיקה נוספת
return <WrappedComponent {...props} extraProp="value" />;
};
};
דוגמה: withLoading
// HOC שמוסיף מצב טעינה
function withLoading(WrappedComponent) {
return function WithLoadingComponent({ isLoading, ...props }) {
if (isLoading) {
return (
<div className="loading">
<span className="spinner">⏳</span>
<p>טוען...</p>
</div>
);
}
return <WrappedComponent {...props} />;
};
}
// קומפוננטה רגילה
function UserList({ users }) {
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
// קומפוננטה משופרת
const UserListWithLoading = withLoading(UserList);
// שימוש
function App() {
const [loading, setLoading] = useState(true);
const [users, setUsers] = useState([]);
return <UserListWithLoading isLoading={loading} users={users} />;
}
דוגמה: withAuth
function withAuth(WrappedComponent) {
return function AuthenticatedComponent(props) {
const { isAuthenticated, loading } = useAuth();
if (loading) {
return <p>בודק הרשאות...</p>;
}
if (!isAuthenticated) {
return <Navigate to="/login" />;
}
return <WrappedComponent {...props} />;
};
}
// שימוש
const ProtectedDashboard = withAuth(Dashboard);
const ProtectedSettings = withAuth(Settings);
דוגמה: withLogger
function withLogger(WrappedComponent) {
return function LoggedComponent(props) {
useEffect(() => {
console.log(`${WrappedComponent.name} mounted`);
return () => console.log(`${WrappedComponent.name} unmounted`);
}, []);
useEffect(() => {
console.log(`${WrappedComponent.name} props:`, props);
});
return <WrappedComponent {...props} />;
};
}
Render Props
Render Props הוא דפוס שבו קומפוננטה מקבלת פונקציה כ-prop (לרוב ה-children) וקוראת לה עם הנתונים הפנימיים שלה. כך הקומפוננטה מנהלת את הלוגיקה, ומי שמשתמש בה שולט בתצוגה - שיתוף לוגיקה תוך שמירה על גמישות מלאה בעיצוב:
התחביר
// קומפוננטה עם render prop
function DataProvider({ render }) {
const [data, setData] = useState(null);
// לוגיקה לקבלת data...
return render(data);
}
// שימוש
<DataProvider
render={(data) => <DisplayComponent data={data} />}
/>
דוגמה: Mouse Position
function MouseTracker({ children }) {
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);
}, []);
// קוראים ל-children כפונקציה עם הנתונים
return children(position);
}
// שימוש
function App() {
return (
<MouseTracker>
{({ x, y }) => (
<div>
<p>מיקום: {x}, {y}</p>
<div
style={{
position: 'absolute',
left: x - 10,
top: y - 10,
width: 20,
height: 20,
background: 'red',
borderRadius: '50%'
}}
/>
</div>
)}
</MouseTracker>
);
}
דוגמה: Toggle
function Toggle({ children }) {
const [isOn, setIsOn] = useState(false);
const toggle = () => setIsOn(prev => !prev);
return children({ isOn, toggle });
}
// שימוש
function App() {
return (
<Toggle>
{({ isOn, toggle }) => (
<div>
<button onClick={toggle}>
{isOn ? 'כבה' : 'הפעל'}
</button>
{isOn && <p>התוכן מוצג!</p>}
</div>
)}
</Toggle>
);
}
Error Boundaries
Error Boundary הוא קומפוננטה שתופסת שגיאות JavaScript שמתרחשות בקומפוננטות שמתחתיה בעץ, ומציגה במקומן ממשק חלופי במקום שכל האפליקציה תקרוס. אפשר לחשוב עליו כמו try/catch ברמת ה-UI.
⚠️ Error Boundary הוא היוצא מן הכלל היחיד שבו עדיין נדרשת קומפוננטת מחלקה (Class Component), משום שהוא מסתמך על מתודות מחזור חיים שאין להן חלופה ב-Hooks.
התחביר
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
// נקרא כשיש שגיאה בילד
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
// לוגים ודיווח
componentDidCatch(error, errorInfo) {
console.error('Error caught:', error);
console.error('Error info:', errorInfo);
// שליחה לשירות דיווח שגיאות
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div className="error-fallback">
<h2>😰 משהו השתבש</h2>
<p>אנא רענן את הדף</p>
<button onClick={() => window.location.reload()}>
רענן
</button>
</div>
);
}
return this.props.children;
}
}
שימוש
function App() {
return (
<ErrorBoundary fallback={<p>שגיאה באפליקציה</p>}>
<Header />
<ErrorBoundary fallback={<p>שגיאה בתוכן</p>}>
<MainContent />
</ErrorBoundary>
<Footer />
</ErrorBoundary>
);
}
// קומפוננטה שעלולה לזרוק שגיאה
function BuggyComponent() {
const [shouldError, setShouldError] = useState(false);
if (shouldError) {
throw new Error('שגיאה מכוונת!');
}
return (
<button onClick={() => setShouldError(true)}>
גרום לשגיאה
</button>
);
}
Error Boundary עם Reset
class ErrorBoundaryWithReset extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
handleReset = () => {
this.setState({ hasError: false });
}
render() {
if (this.state.hasError) {
return (
<div>
<h2>אופס! משהו השתבש</h2>
<button onClick={this.handleReset}>נסה שוב</button>
</div>
);
}
return this.props.children;
}
}
Fragments
כבר פגשנו את Fragment בשיעור על JSX. כתזכורת: הוא מאפשר לקבץ כמה אלמנטים תחת אלמנט שורש יחיד, בלי להוסיף תגית עוטפת מיותרת ל-DOM - מה שחשוב במיוחד בהקשרים כמו טבלאות ורשימות, שבהם div נוסף עלול לשבור את המבנה:
תחביר
// תחביר מלא
import { Fragment } from 'react';
function List() {
return (
<Fragment>
<li>פריט 1</li>
<li>פריט 2</li>
<li>פריט 3</li>
</Fragment>
);
}
// תחביר מקוצר (נפוץ יותר)
function List() {
return (
<>
<li>פריט 1</li>
<li>פריט 2</li>
<li>פריט 3</li>
</>
);
}
Fragment עם Key
// כשצריך key, חייבים תחביר מלא
function Glossary({ items }) {
return (
<dl>
{items.map(item => (
<Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.description}</dd>
</Fragment>
))}
</dl>
);
}
למה להשתמש ב-Fragments?
// ❌ בעייתי - div מיותר שמשפיע על CSS
function Columns() {
return (
<div> {/* div מיותר! */}
<td>עמודה 1</td>
<td>עמודה 2</td>
</div>
);
}
// ✅ נכון - Fragment לא יוצר אלמנט
function Columns() {
return (
<>
<td>עמודה 1</td>
<td>עמודה 2</td>
</>
);
}
השוואה: HOC vs Render Props vs Custom Hooks
| דפוס | יתרונות | חסרונות | מתי להשתמש |
|---|---|---|---|
| HOC | שימוש חוזר, הפרדה | Wrapper hell, props conflict | Cross-cutting concerns |
| Render Props | גמישות, שקיפות | Callback hell, פחות קריא | UI דינמי מאוד |
| Custom Hooks | פשוט, קריא, מודרני | רק לוגיקה (לא UI) | מועדף ברוב המקרים |
המרה מ-HOC ל-Custom Hook
// HOC (ישן)
function withWindowSize(WrappedComponent) {
return function(props) {
const [size, setSize] = useState(getSize());
useEffect(() => { /* listener */ }, []);
return <WrappedComponent {...props} windowSize={size} />;
};
}
// Custom Hook (מודרני ומועדף)
function useWindowSize() {
const [size, setSize] = useState(getSize());
useEffect(() => { /* listener */ }, []);
return size;
}
סיכום
| Pattern | תחביר |
|---|---|
| HOC | withX(Component) |
| Render Props | <Provider>{(data) => ...}</Provider> |
| Error Boundary | Class with getDerivedStateFromError |
| Fragment | <>...</> או <Fragment> |
הקודם: ← Custom Hooks
הבא: React Router →
בדקו את עצמכם
נסו לענות לבד לפני שאתם פותחים את התשובה.
-
מה תופס Error Boundary?
הצגת התשובה
תשובה ג. הוא לא תופס שגיאות ב-event handlers או בקוד אסינכרוני.
-
מה החליף ברוב המקרים את HOC ו-Render Props?
הצגת התשובה
תשובה ב. Hooks משתפים לוגיקה בלי לעטוף קומפוננטות ברמות נוספות.