Menu

ההוק useContext ב-React: שיתוף נתונים בלי props

useContext קורא ערך שקומפוננטת הורה מספקת, כך שקומפוננטה מקוננת עמוק מקבלת אותו בלי שכל קומפוננטה בדרך תעביר אותו מטה כ-prop. למדו createContext, providers, עדכון קונטקסט מילד, ואיך קונטקסט משפיע על רינדורים מחדש.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

useContext קורא ערך שקומפוננטת הורה מספקת, כך שקומפוננטה עמוק בעץ יכולה לקבל אותו בלי שכל קומפוננטה בדרך תעביר אותו כ-prop. יוצרים קונטקסט עם createContext, עוטפים חלק מהעץ ב-provider עם value, וקוראים ל-useContext(TheContext) בכל מקום שבו צריך את הערך.

Toolbar אף פעם לא מזכירה את ערכת הנושא, ובכל זאת שני הכפתורים משתנים כשמסמנים את התיבה. זה כל הרעיון של קונטקסט: הקומפוננטות באמצע לא נושאות prop שהן לא משתמשות בו, וזו בעיית ה-prop drilling שקונטקסט פותר.

שלושת הצעדים

// 1. Create it once, outside any component, usually in its own file
export const ThemeContext = createContext('light');

// 2. Provide a value to a part of the tree
<ThemeContext value={theme}>
    <Page />
</ThemeContext>

// 3. Read it in any component inside that part
const theme = useContext(ThemeContext);

ב-React 19 אובייקט הקונטקסט עצמו הוא ה-provider. קוד ישן כותב <ThemeContext.Provider value={theme}>, שעדיין עובד, אז תראו את שתי הצורות.

useContext מחפש למעלה בעץ את ה-provider הקרוב ביותר של אותו קונטקסט. קומפוננטה קוראת את הקרוב ביותר, כך שאפשר לקנן providers כדי לדרוס ערך עבור אזור אחד.

ערך ברירת המחדל

הארגומנט של createContext הוא מה ש-useContext מחזיר כשאין provider מעל הקומפוננטה. הוא לא משתנה כש-state משתנה; הוא ערך גיבוי.

ל-Greeting הראשונה אין provider מעליה והיא מקבלת את ברירת המחדל 'en'. השנייה קוראת 'es', והשלישית קוראת את ה-'ja' הקרוב יותר. שנו את value="es" החיצוני ל-value="ja" ורק השורה השנייה משתנה.

עדכון קונטקסט מילד

קונטקסט רק מעביר ערך מטה. כדי לאפשר לילד עמוק לשנות אותו, החזיקו את הערך ב-state במקום שבו נמצא ה-provider, ושימו את ה-setter בקונטקסט לידו.

LoginButton קוראת ל-setUser, ה-state ב-App משתנה, ה-provider מקבל ערך חדש, וגם Header מתעדכנת. אף אחת מהקומפוננטות לא קיבלה prop.

כל צרכן מתרנדר מחדש כשהערך משתנה

כשה-value של provider משתנה, React מרנדרת כל קומפוננטה שקוראת לו useContext, גם אם הקומפוננטה הזו יושבת בתוך memo וה-props שלה נשארו אותו דבר. React משווה את הערך הישן לחדש עם Object.is. לכן value={{ user, setUser }} הוא אובייקט חדש בכל רינדור של App, וכל צרכן מתרנדר שוב גם כש-user לא השתנה.

שני הקוראים רושמים פעם אחת ברינדור הראשון. לחצו על הכפתור: רק PlainReader רושם שוב, כי הקונטקסט שלו קיבל אובייקט חדש. useMemo נותן ל-MemoContext את אותו אובייקט עד ש-name משתנה, ולכן MemoReader נשאר במקום. זה משנה רק כשהרינדור נהיה איטי; באפליקציה קטנה, אובייקט חדש בכל רינדור זה בסדר.

פיצול קונטקסטים

ערך שמשתנה הרבה וערך שאף פעם לא משתנה לא צריכים לחלוק קונטקסט. אם ה-setter חי עם ה-state, קומפוננטה שרק צריכה לקרוא ל-setUser עדיין מתרנדרת בכל פעם ש-user משתנה. שימו אותם בשני קונטקסטים וכל צרכן נרשם רק למה שהוא קורא:

const UserContext = createContext(null);
const SetUserContext = createContext(() => {});

function UserProvider({ children }) {
    const [user, setUser] = useState(null);
    return (
        <SetUserContext value={setUser}>
            <UserContext value={user}>{children}</UserContext>
        </SetUserContext>
    );
}

setUser מ-useState שומר על אותה זהות לנצח, ולכן קומפוננטות שקוראות רק את SetUserContext אף פעם לא מתרנדרות בגלל התחברות. אותו רעיון חל על נתונים לא קשורים: ערכת נושא ועגלת קניות שייכות לקונטקסטים נפרדים, לא ל-AppContext גדול אחד.

נגיעה אחרונה נפוצה היא הוק מותאם אישית כמו useUser() שקורא ל-useContext(UserContext) וזורק שגיאה ברורה כשה-provider חסר, כך שמי שקורא לו אף פעם לא מייבא את אובייקט הקונטקסט ישירות.

קונטקסט, props או ספריית state

  • props הם ברירת המחדל. העברת ערך שתיים או שלוש רמות מטה ברורה וקל לעקוב אחריה.
  • קונטקסט מתאים לערכים שהרבה קומפוננטות צריכות בהרבה עומקים ושמשתנים לעיתים רחוקות: ערכת נושא, שפה, המשתמש המחובר, אובייקט של feature flags.
  • ספריית state (Redux Toolkit, Zustand, Jotai ואחרות) מתאימה ל-state גדול שמתעדכן הרבה, כשרוצים שקומפוננטה תירשם לחלק אחד ותדלג על רינדורים שנגרמים מהשאר. לקונטקסט אין selector כזה: צרכן מתרנדר בכל שינוי בערך.

לפני שפונים לקונטקסט או לספרייה, בדקו אם העברת קומפוננטות בתור children מבטלת את ה-drilling. לעיתים קרובות כן, בלי שום API חדש.

שאלות נפוצות

מה useContext עושה ב-React?

הוא מחזיר את הערך הנוכחי של קונטקסט, שנלקח מה-provider הקרוב ביותר מעל הקומפוננטה בעץ. כשהערך של ה-provider הזה משתנה, כל קומפוננטה שקוראת אותו מתרנדרת שוב.

בשביל מה ערך ברירת המחדל ב-createContext?

זה מה ש-useContext מחזיר כשאין provider מעל הקומפוננטה. הוא שימושי לבדיקות ולקומפוננטות שמרונדרות לבד, והוא אף פעם לא משתנה.

האם עדיין צריך Context.Provider ב-React 19?

לא. ב-React 19 אפשר לרנדר את הקונטקסט עצמו בתור ה-provider: <ThemeContext value="dark">. <ThemeContext.Provider value="dark"> עדיין עובד, כך שקוד ישן ממשיך לרוץ.

איך מעדכנים ערך של קונטקסט מקומפוננטת ילד?

מחזיקים את הערך ב-state בקומפוננטה שמרנדרת את ה-provider, ושמים את ה-setter בקונטקסט ליד הערך: value={{ theme, setTheme }}. הילד קורא את setTheme עם useContext וקורא לו.

האם קונטקסט של React מחליף את Redux?

קונטקסט מעביר ערך מטה בעץ; הוא לא מנהל state בעצמו. בשילוב עם useState או useReducer הוא מכסה הרבה אפליקציות. ספריית state מוסיפה דברים שאין בקונטקסט, כמו הרשמה לחלק אחד כך שעדכונים לא קשורים לא מרנדרים קומפוננטה מחדש, ועוד devtools ו-middleware.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל