תבניות מתקדמות (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 →

בדקו את עצמכם

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

  1. מה תופס Error Boundary?

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

    תשובה ג. הוא לא תופס שגיאות ב-event handlers או בקוד אסינכרוני.

  2. מה החליף ברוב המקרים את HOC ו-Render Props?

    1. מחלקות
    2. Custom Hooks
    3. jQuery
    4. Redux
    הצגת התשובה

    תשובה ב. Hooks משתפים לוגיקה בלי לעטוף קומפוננטות ברמות נוספות.