Context API

מבוא

Context API הוא המנגנון המובנה של React לשיתוף נתונים "גלובליים" בתוך עץ הקומפוננטות, והפתרון הישיר לבעיית ה-Props Drilling. במקום להעביר prop יד ביד דרך כל רמת ביניים, מגדירים Context פעם אחת - וכל קומפוננטה בתוך העץ יכולה לגשת אליו ישירות. הוא מתאים במיוחד לנתונים שנצרכים במקומות רבים, כמו ערכת נושא, שפה או פרטי המשתמש המחובר.

ללא Context:          עם Context:
App ──► Layout         App (Provider)
    ──► Sidebar            ├── Layout
        ──► UserCard       ├── Sidebar
            ──► Avatar         └── Avatar (Consumer)
                                   ↑ נגיש ישירות!

שלושת השלבים

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

1. Create - יצירת Context

import { createContext } from 'react';

// יצירת Context עם ערך ברירת מחדל
const ThemeContext = createContext('light');

// או בלי ערך ברירת מחדל
const UserContext = createContext(null);

2. Provide - הספקת הערך

import { ThemeContext } from './ThemeContext';

function App() {
    const [theme, setTheme] = useState('dark');

    return (
        // Provider עוטף את כל הקומפוננטות שצריכות גישה
        <ThemeContext.Provider value={theme}>
            <Header />
            <Main />
            <Footer />
        </ThemeContext.Provider>
    );
}

3. Consume - צריכת הערך

import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';

function ThemedButton() {
    // שימוש ב-useContext לקבלת הערך
    const theme = useContext(ThemeContext);

    return (
        <button className={`btn-${theme}`}>
            כפתור ב-{theme} mode
        </button>
    );
}

דוגמה מלאה: Theme Context

קובץ ThemeContext.jsx

import { createContext, useContext, useState } from 'react';

// יצירת Context
const ThemeContext = createContext(null);

// Provider Component
export function ThemeProvider({ children }) {
    const [theme, setTheme] = useState('light');

    const toggleTheme = () => {
        setTheme(prev => prev === 'light' ? 'dark' : 'light');
    };

    // ערך ה-Context כולל את ה-state והפונקציות
    const value = {
        theme,
        toggleTheme,
        isDark: theme === 'dark'
    };

    return (
        <ThemeContext.Provider value={value}>
            {children}
        </ThemeContext.Provider>
    );
}

// Custom Hook לשימוש נוח
export function useTheme() {
    const context = useContext(ThemeContext);

    if (context === null) {
        throw new Error('useTheme must be used within ThemeProvider');
    }

    return context;
}

שימוש

// App.jsx
import { ThemeProvider } from './ThemeContext';

function App() {
    return (
        <ThemeProvider>
            <Header />
            <Main />
        </ThemeProvider>
    );
}

// Header.jsx
import { useTheme } from './ThemeContext';

function Header() {
    const { theme, toggleTheme } = useTheme();

    return (
        <header className={theme}>
            <button onClick={toggleTheme}>
                {theme === 'light' ? '🌙' : '☀️'}
            </button>
        </header>
    );
}

// ThemedCard.jsx
import { useTheme } from './ThemeContext';

function ThemedCard({ children }) {
    const { isDark } = useTheme();

    return (
        <div style={{
            backgroundColor: isDark ? '#333' : '#fff',
            color: isDark ? '#fff' : '#333',
            padding: '20px'
        }}>
            {children}
        </div>
    );
}

דוגמה: Auth Context

import { createContext, useContext, useState, useEffect } from 'react';

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
    const [user, setUser] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        // בדיקת משתמש מחובר בטעינה
        const savedUser = localStorage.getItem('user');
        if (savedUser) {
            setUser(JSON.parse(savedUser));
        }
        setLoading(false);
    }, []);

    const login = async (email, password) => {
        // קריאה ל-API
        const response = await fetch('/api/login', {
            method: 'POST',
            body: JSON.stringify({ email, password })
        });
        const userData = await response.json();

        setUser(userData);
        localStorage.setItem('user', JSON.stringify(userData));
    };

    const logout = () => {
        setUser(null);
        localStorage.removeItem('user');
    };

    const value = {
        user,
        isAuthenticated: !!user,
        loading,
        login,
        logout
    };

    return (
        <AuthContext.Provider value={value}>
            {children}
        </AuthContext.Provider>
    );
}

export function useAuth() {
    const context = useContext(AuthContext);
    if (!context) {
        throw new Error('useAuth must be used within AuthProvider');
    }
    return context;
}

// שימוש בקומפוננטות
function LoginButton() {
    const { isAuthenticated, logout } = useAuth();

    if (isAuthenticated) {
        return <button onClick={logout}>התנתק</button>;
    }
    return <a href="/login">התחבר</a>;
}

function ProtectedRoute({ children }) {
    const { isAuthenticated, loading } = useAuth();

    if (loading) return <p>טוען...</p>;
    if (!isAuthenticated) return <Navigate to="/login" />;

    return children;
}

שילוב מספר Contexts

function App() {
    return (
        <AuthProvider>
            <ThemeProvider>
                <LanguageProvider>
                    <Router>
                        <MainApp />
                    </Router>
                </LanguageProvider>
            </ThemeProvider>
        </AuthProvider>
    );
}

// קומפוננטה שמשתמשת במספר contexts
function Dashboard() {
    const { user } = useAuth();
    const { theme } = useTheme();
    const { language } = useLanguage();

    return (
        <div className={theme}>
            <h1>{language === 'he' ? 'שלום' : 'Hello'}, {user.name}</h1>
        </div>
    );
}

Context Performance

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

בעיה

// ❌ כל שינוי ב-count גורם לרינדור של כל הצרכנים
function AppProvider({ children }) {
    const [user, setUser] = useState(null);
    const [count, setCount] = useState(0);

    return (
        <AppContext.Provider value={{ user, setUser, count, setCount }}>
            {children}
        </AppContext.Provider>
    );
}

פתרון: הפרדת Contexts

// ✅ הפרדה לפי תחום
const UserContext = createContext(null);
const CounterContext = createContext(null);

function Providers({ children }) {
    return (
        <UserProvider>
            <CounterProvider>
                {children}
            </CounterProvider>
        </UserProvider>
    );
}

פתרון: useMemo

function AppProvider({ children }) {
    const [user, setUser] = useState(null);

    // מניעת יצירת אובייקט חדש בכל רינדור
    const value = useMemo(() => ({ user, setUser }), [user]);

    return (
        <AppContext.Provider value={value}>
            {children}
        </AppContext.Provider>
    );
}

Consumer (סגנון ישן)

לפני שהגיע ה-Hook בשם useContext, צרכו את ה-Context בעזרת רכיב Consumer ופונקציית render. התחביר עדיין נתמך, אך useContext קריא ונוח בהרבה ומומלץ לכל קוד חדש:

// סגנון ישן עם Consumer
<ThemeContext.Consumer>
    {theme => (
        <div className={theme}>תוכן</div>
    )}
</ThemeContext.Consumer>

// סגנון מודרני עם useContext
function Component() {
    const theme = useContext(ThemeContext);
    return <div className={theme}>תוכן</div>;
}

סיכום

פעולה קוד
יצירה const Ctx = createContext(default)
הספקה <Ctx.Provider value={val}>
צריכה const val = useContext(Ctx)
Custom Hook useMyContext() עם בדיקת שגיאה

הקודם: ← Component Patterns
הבא: Custom Hooks →

בדקו את עצמכם

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

  1. מתי כדאי להשתמש ב-Context?

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

    תשובה ג. Context פותר Props Drilling לנתונים גלובליים באמת.

  2. מה החיסרון של ערך Context שמשתנה הרבה?

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

    תשובה ב. לכן מפצלים Contexts, ולא שמים שם state שמשתנה כל שנייה.