העברת props דרך שכבות של רכיבים שאינם זקוקים להם היא עבודה משעממת. יום אחד אתם משיקים פיצ'ר, וביום למחרת אתם עורכים שישה קבצים רק כדי לשנות שם של prop בודד. זהו ה-Prop Drilling בתמצית: להורה יש נתונים, לילד עמוק יש צורך בהם, וכל רכיב ביניהם הופך לשליח. האפליקציה עדיין רצה, אבל בסיס הקוד הופך לשביר. הסרת שכבת ביניים אחת, וכל העץ קורס. שינוי של type, ו-TypeScript מתלונן בשלושה תיקיות שונות. React Context API קיים כדי להסיר את המתווכים הללו לחלוטין.

איך Prop Drilling נראה בפועל

דמיינו שלד אפליקציה סטנדרטי. יש לכם רכיב App שמביא את המשתמש הנוכחי. בתוך App חי Layout, בתוך Layout יושב Sidebar, בתוך Sidebar מקונן Navigation, ולבסוף בתוך Navigation תמצאו את ה-UserAvatar שבאמת זקוק לאובייקט המשתמש.

הקוד שלכם ייראה כך:

function App() {
  const user = { name: 'Aarav', role: 'admin' };
  return <Layout user={user} />;
}

function Layout({ user }) {
  return <Sidebar user={user} />;
}

function Sidebar({ user }) {
  return <Navigation user={user} />;
}

function Navigation({ user }) {
  return <UserAvatar user={user} />;
}

Layout, Sidebar, ו-Navigation לא עושים דבר עם אובייקט המשתמש הזה מלבד להעביר אותו הלאה. הם צוברים props שהם לא הבעלים שלהם, הממשקים (interfaces) שלהם מתנפחים, ובדיקתם דורשת mocking של נתונים שהם מעולם לא נוגעים בהם. הפשע האמיתי הוא כמה מהר זה מתפשט. הוסיפו flag של isLoggedIn, מחרוזת locale, או ערך theme, והמצעד הזה חוזר על עצמו.

איך Context API משנה את חוקי המשחק

חשבו על Context API כמו על נתב WiFi. במקום להריץ כבלים ארוכים דרך כל חדר כדי להגיע לכל מכשיר, הנתב שולח אות באוויר. כל מכשיר בטווח יכול להתחבר ישירות. במונחים של React, הנתב הוא ה-Provider, האות הוא ה-state או הנתונים שלכם, והמכשיר הוא כל רכיב מקונן שקורא ל-useContext.

ההגדרה כוללת שלושה חלקים:

  • React.createContext() בונה את ערוץ הנתונים.
  • ה-Provider עוטף חלק מהעץ שלכם ומעביר ערך.
  • ה-hook של useContext מאפשר לצאצאים לקבל את הערך הזה מבלי לגעת ב-props הביניים.

עדיין יש לכם עץ, אבל הענפים בין השורש לעלה כבר לא צריכים להסכים על "חוזה שליח".

בניית Context מאפס

בואו נבנה דוגמה קונקרטית עם הגדרות theme, מכיוון שרוב האפליקציות זקוקות למצב בהיר או כהה בשלב כלשהו.

ראשית, צרו את אובייקט ה-context. זהו הצינור:

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

const ThemeContext = createContext(null);

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

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

export default ThemeContext;

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

import { ThemeProvider } from './ThemeContext';

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

כעת כל צאצא יכול "להתחבר" לאות ישירות. הנה כפתור toggle קבור עמוק בתוך ה-UI:

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

function ThemeToggle() {
  const { theme, setTheme } = useContext(ThemeContext);

  return (
    <button
      onClick={() => setTheme(prev => prev === 'light' ? 'dark' : 'light')}
    >
      Current theme: {theme}
    </button>
  );
}

שימו לב ש-Layout, Sidebar, ו-Navigation לעולם לא רואים את ה-theme prop. הם מתרנדרים כרגיל, ו-ThemeToggle לוקח את מה שהוא צריך ישירות מה-context. החיווט בלתי נראה מבחוץ, וזה בדיוק העניין.

איפה Context באמת מתאים

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

  • הגדרות theme כמו מצב בהיר או כהה, צבעי דגש (accent colors), או גודל גופן.
  • מצב אימות (Authentication state) כמו אובייקט המשתמש הנוכחי, סטטוס התחברות, או פקיעת תוקף הסשן.
  • שפה ו-locale עבור בינלאומיות (internationalization).
  • נתוני עגלת קניות שחייבים להישאר מסונכרנים בין תגית בכותרת (header badge), תפריט עגלה מיניאטורי, ודף תשלום.

התנגדו לדחף להכניס כל פיסת state מקומי לתוך Context. קלט של טופס שתי רמות למטה לא זקוק לשידור גלובלי. שמרו את ה-Context לנושאים חוצי-רכיבים (cross-cutting concerns) אמיתיים, ותנו לשאר להישאר כ-props רגילים.

מלכודות ביצועים ואיך להימנע מהן

Context אינו בחינם. כאשר ערך של context מתעדכן, כל רכיב המחובר ל-context הזה מתרנדר מחדש, גם אם חלק הערך שמעניין אותו לא השתנה. הטעות הקלאסית היא להשליך אובייקט ליטרלי חדש לתוך ה-Provider בכל רינדור של ההורה.

בדוגמת ה-theme שלנו, בכל פעם ש-ThemeProvider מתרנדר מחדש כי ההורה שלו התעדכן, הביטוי { theme, setTheme } יוצר אובייקט חדש לגמרי. React רואה רפרנס חדש, וכל הצרכנים (consumers) מתעדכנים. אם ה-theme שלכם משתנה לעיתים רחוקות אך ה-state של האפליקציה משתנה לעיתים קרובות, אתם משלמים על רינדורים שאתם לא צריכים.

הפתרון הוא דו-שלבי.

פצלו את ה-contexts שלכם לפי תדירות עדכון. UserContext שמשתנה פעם אחת בכל התחברות לא צריך לחלוק provider עם NotificationContext שמתעדכן כל כמה שניות. שמרו אותם נפרדים כדי שנתונים סטטיים לא ירכבו על אותה "רכבת רינדורים" של נתונים משתנים.

עטפו את הערך ב-useMemo כאשר הערך הוא אובייקט או מערך. תנו ל-React רפרנס יציב:

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

  const value = useMemo(() => ({ theme, setTheme }), [theme]);

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

כעת זהות האובייקט משתנה רק כאשר ה-theme משתנה בפועל. רכיבי צאצאים שמתעניינים ב-context אך מוגנים על ידי React.memo בהמשך הדרך ידלגו על העבודה.

טעויות שמבזבזות לכם את הזמן

שתי השגיאות שעדיין מופיעות בקוד ב-production הן קלות למניעה.

ראשית, שכחה לייצא את ה-context עצמו. אם אתם מייצאים רק את ה-Provider wrapper ושומרים על אובייקט ה-context פרטי, מפתח שכותב פיצ'ר חדש לא יוכל לקרוא ל-useContext מבלי לבצע refactoring למודול שלכם. ייצאו את ה-context כדי שצרכנים יוכלו לייבא גם את ה-provider וגם את ה-consumer hook בצורה נקייה.

שנית, קריאה ל-useContext מחוץ ל-Provider המתאים. אם ThemeToggle מרונדר בענף של העץ שאינו עטוף ב-ThemeProvider, ה-hook יחזיר את ערך ברירת המחדל שהועבר ל-createContext, או undefined אם לא העברתם דבר. זה מוביל לכשלים שקטים כמו cannot read property of undefined. ניתן להגן על כך על ידי הקצאת ערך ברירת מחדל הגיוני או זריקת שגיאה ברורה בשלב מוקדם בקריאה ל-hook.

Context לעומת Redux: שמרו על זה פשוט

אתם לא תמיד צריכים Redux. עבור פרויקטים בדרגת גודל בינונית, Context בשילוב עם useState או useReducer מכסה את רוב שיתוף ה-state. Redux מצטיין כשצריכים time-travel debugging, middleware מורכב, או טרנזקציות גלובליות שחייבות לבצע rollback ברצף. אם כל סיפור ה-state שלכם הוא אובייקט משתמש, מחרוזת theme ומערך עגלה, ספריית store רק תוסיף boilerplate שלעולם לא תנצלו.

עם זאת, Context אינו מערכת ניהול state מלאה בפני עצמה. הוא לא נותן לכם snapshot גלובלי יחיד, והוא לא מבצע batching לעדכונים בין contexts לא קשורים. השתמשו בו כתחליף ל-prop drilling, ולא כמערכת הפעלה עבור כל שכבת הנתונים שלכם.

השורה התחתונה

הפסיקו להעביר props דרך רכיבים שלא אכפת להם מהם. צרו contexts ממוקדים עבור הנתונים שבאמת חוצים את העץ שלכם, עטפו providers מספיק גבוה כדי לכסות את הצרכנים, ותמיד ייצבו את אובייקט הערך כשאתם מעבירים אוספים (collections) או פונקציות. ה-Context API שומר על קוד ה-React שלכם ישיר: ה-props נשארים מקומיים, נתונים גלובליים עוברים "אלחוטית", וגבולות הרכיבים שלכם נשארים נקיים.