קונטקסט

קונטקסט מספק דרך להעביר מידע דרך עץ הקומפוננטות בלי להשתמש ב-props באופן ידני לכל קומפוננטה.

באפליקציית React טיפוסית, המידע מועבר למטה (מקומפוננטת אב לקומפוננטת ילד) דרך props, אבל עבור מידע שנדרש בהרבה קומפוננטות באפליקציה (כמו לדוגמא העדפות שפה או ערכת נושא של ממשק המשתמש) השימוש ב-props יכול להיות מסורבל. קונטקסט מספק דרך לשתף מידע כזה בין קומפוננטות בלי להעביר אותו באופן מפורש לכל קומפוננטה.

מתי להשתמש בקונטקסט

הקונטקסט נועה לשתף מידע שנחשב ״גלובאלי״ לכל הקומפוננטות בעץ, כמו מידע על המשתמש המאומת, ערכת הנושא או השפה המועדפת. בקוד הנ״ל אנחנו מעבירים את ה-prop של ״ערכת הנושא״ בשביל לעצב את קומפוננטת הכפתור:

class App extends React.Component {
  render() {
    return <Toolbar theme="dark" />;
  }
}

function Toolbar(props) {
  // "ערכת נושא" prop-קומפוננטת התפריט צריכה לקבל את ה
  // זה יכול להפוך את הקוד ."ThemedButton" ולהעביר אותו לכפתור
  // למסורבל אם כל כפתור באפליקציה צריך לדעת על ערכת הנושא בצורה זו
  // כי נאלץ להעביר אותה דרך כל הקומפוננטות.
  return (
    <div>
      <ThemedButton theme={props.theme} />
    </div>
  );
}

class ThemedButton extends React.Component {
  render() {
    return <Button theme={this.props.theme} />;
  }
}

בעזרת הקונטקסט, אפשר להמנע מלהעביר את ה-prop דרך רכיבי ביניים:

// קונטקס נותן לנו להעביר ערך עמוק לתוך עץ הקומפוננטות
// בלי להעביר אותו באופן מפורש לכל קומפוננטה.
// (צור קונטקס לערכת הנושא (עם ברירת מחדל ״בהירה״
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    // כדי להעביר את ערכת הנושא לעץ הקומפוננטות Provider-נשתמש ב
    // כל קומפוננטה יכולה להשתמש בו, לא משנה באיזה עומק.
    // בדוגמא הבאה, נעביר ״כהה״ כערך לערכת הנושא.
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// קומפוננטת ביניים כבר לא צריכה להעביר
// את ערכת הנושא.
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // contextType-נקרא את ערך ערכת הנושא מהקונטקס ב.
  // ימצא את ספק ערכת הנושא הקרוב ויקרא את ערך ערכת הנושא React
  // בדוגמא הזאת, הערך הוא ״כהה״.
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}

לפני השימוש בקונטקסט

השימוש בקונטקסט נועד בעיקר למצב שבו חלק מהמידע צריך להיות נגיש להרבה קומפוננטות בעומקים שונים. עדיף להשתמש בקונטקסט בחסכנות כי הוא יכול להקשות על שימוש חוזר בקומפוננטות.

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

לדוגמא, קומפוננטת Page שמעבירה את ה-props user ו- avatarSize לכמה רמות עומק, כדי שקומפוננטות ילד כמו Link ו- Avatar יוכלו להשתמש בהם:

<Page user={user} avatarSize={avatarSize} />
// ... שמרנדרת ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... שמרנדרת ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ... שמרנדרת ...
<Link href={user.permalink}>
  <Avatar user={user} size={avatarSize} />
</Link>

יכול להיות שהעברת ה-props user ו- avatarSize דרך כל כך הרבה רמות עומק תרגיש מיותר, בעיקר כי בסוף רק קומפוננטת ה- Avatar באמת משתמשת בהם. זה גם מעצבן שכל פעם שקומפוננטת ה- Avatar צריכה עוד props, צריך להעביר אותם דרך כל רכיבי הביניים.

דרך אחת לפתור את הבעיה ללא שימוש בקונטקסט היא להעביר את קומפוננטת ה-Avatar עצמה כדי שקומפוננטות הביניים לא יצטרכו לדעת על ה-props user או avatarSize:

function Page(props) {
  const user = props.user;
  const userLink = (
    <Link href={user.permalink}>
      <Avatar user={user} size={props.avatarSize} />
    </Link>
  );
  return <PageLayout userLink={userLink} />;
}

// :עכשיו, יש לנו
<Page user={user} avatarSize={avatarSize} />
// ... שמרנדרת ...
<PageLayout userLink={...} />
// ... שמרנדרת ...
<NavigationBar userLink={...} />
// ... שמרנדרת ...
{props.userLink}

עם השינוי הזה, רק הקומפוננטה העליונה Page צריכה לדעת על קומפוננטות ה- Link וה- Avatar ועל ה-props שהן דורשות.

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

אין הגבלה של ילד יחיד לכל קומפוננטה. אפשר להעביר מספר ילדים, ואפילו מספר ״משבצות״ (“slots”) לילדים, כמתועד כאן:

function Page(props) {
  const user = props.user;
  const content = <Feed user={user} />;
  const topBar = (
    <NavigationBar>
      <Link href={user.permalink}>
        <Avatar user={user} size={props.avatarSize} />
      </Link>
    </NavigationBar>
  );
  return (
    <PageLayout
      topBar={topBar}
      content={content}
    />
  );
}

תבנית העיצוב הזאת מספיקה במקרים רבים כשרוצים להפריד קומפוננטת ילד מקומפוננטת האב שלה. אפשר להרחיב את הפתרון עוד יותר עם render props במקרים שקומפוננטת הילד צריכה לתקשר עם קומפוננטת האב לפני הרינדור.

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

ממשק תכנות

React.createContext

const MyContext = React.createContext(defaultValue);

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

ערך ברירת המחדל נקרא רק כשאין ספק מעליו בעץ הקומפוננטות. זה יכול להיות שימושי בבדיקות אוטומטיות לקומפוננטה בבידוד - בלי צורך לעטוף אותן. הערה: העברת הערך undefined כערך לספק לא יגרום להחזרת ערך ברירת המחדל.

Context.Provider

<MyContext.Provider value={/* some value */}>

כל עצם קונטקסט מגיע עם קומפוננטת ספק (Provider) שנותנת לקומפוננטות שצורכות אותו להקשיב לשינויים בקונטקסט. הספק מקבל prop value שיועבר לקומפוננטות ילד שצורכות את הספק בכל רמות העומק של העץ. ספק אחד יכול להתחבר לצרכנים רבים. אפשר להגדיר ספקים ברמות שונות של אותו העץ כדי לעקוף את הערכים המוגדרים בהם בעומקים שונים של עץ הקומפוננטות.

כל צרכן שהוא צאצא של הספק ירנדר את עצמו מחדש כשה- prop value של הספק משתנה. התפשטות מהספק לצאצאים לא נתונה לחסות המתודה shouldComponentUpdate, ולכן הצרכנים מתעדכנים אפילו כשקומפוננטת אב לא מקשיבה לעדכון.

שינויים נקבעים ע״י השוואת הערכים החדשים מול הישנים בעזרת אותו האלגוריתם כמו Object.is.

הערה

הדרך שבה שינויים נקבעים יכולה ליצור בעיות כשמעבירים עצמים כערכים: ראה הסתיגויות.

Class.contextType

class MyClass extends React.Component {
  componentDidMount() {
    let value = this.context;
    /* MyContext-בשימוש בערך ה mount-יגרום לתופעת לואי בזמן ה */
  }
  componentDidUpdate() {
    let value = this.context;
    /* ... */
  }
  componentWillUnmount() {
    let value = this.context;
    /* ... */
  }
  render() {
    let value = this.context;
    /*MyContext-ירנדר משהו שמבוסס על ערך ה */
  }
}
MyClass.contextType = MyContext;

מאפיין ה-contextType במחלקה מוקצה בעצם קונטקסט שנוצר על ידי המתודה React.createContext().זה נותן לנו לצרוך את ערך הקונטקסט הנוכחי הקרוב ביותר בעזרת this.context. אפשר להשתמש בהפניה זו בכל אחת ממתודות מחזור החיים כולל מתודת הרינדור.

הערה:

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

אם אתם משתמשים בסינטקסט הנסיוני של מאפייני מחלקה ציבורית, תוכלו להשתמש במאפיין מחלקה סטטי על מנת לאתחל את ה- contextType.

class MyClass extends React.Component {
  static contextType = MyContext;
  render() {
    let value = this.context;
    /* רינדור משהו בהתאם לערך */
  }
}

Context.Consumer

<MyContext.Consumer>
  {value => /* רינדור משהו בהתאם לערך */}
</MyContext.Consumer>

קומפוננטת React שמקשיבה לשינויים בקונטקסט. מאפשרת לצרוך קונטקסט מתוך קומפוננטת פונקציה.

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

הערה

למידע נוסף על ״פונקציות כילד״ בקרו בעמוד render props.

דוגמאות

קונטקסט דינאמי

דוגמא מורכבת יותר עם ערך דינאמי של ערכת הנושא:

theme-context.js

export const themes = {
  light: {
    foreground: '#000000',
    background: '#eeeeee',
  },
  dark: {
    foreground: '#ffffff',
    background: '#222222',
  },
};

export const ThemeContext = React.createContext(
  themes.dark // ברירת מחדל
);

themed-button.js

import {ThemeContext} from './theme-context';

class ThemedButton extends React.Component {
  render() {
    let props = this.props;
    let theme = this.context;
    return (
      <button
        {...props}
        style={{backgroundColor: theme.background}}
      />
    );
  }
}
ThemedButton.contextType = ThemeContext;

export default ThemedButton;

app.js

import {ThemeContext, themes} from './theme-context';
import ThemedButton from './themed-button';

// ThemedButton-רכיב ביניים שמשתמש ב
function Toolbar(props) {
  return (
    <ThemedButton onClick={props.changeTheme}>
      Change Theme
    </ThemedButton>
  );
}

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      theme: themes.light,
    };

    this.toggleTheme = () => {
      this.setState(state => ({
        theme:
          state.theme === themes.dark
            ? themes.light
            : themes.dark,
      }));
    };
  }

  render() {
    // בתוך ספק ערכת הנושא ThemedButton-כפתור ה
    // והכפתור החיצוני ,state-משתמש בערכת הנושא מה
    // משתמש בברירת המחדל של ערכת הנושא הכהה
    return (
      <Page>
        <ThemeContext.Provider value={this.state.theme}>
          <Toolbar changeTheme={this.toggleTheme} />
        </ThemeContext.Provider>
        <Section>
          <ThemedButton />
        </Section>
      </Page>
    );
  }
}

ReactDOM.render(<App />, document.root);

עדכון הקונטקסט מתוך קומפוננטה מקוננת

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

theme-context.js

// createContext-שים לב שצורת הערך של ברירת המחדל שמועבר ל
// זהה לצורת הערכים שהצרכנים מצפים לקבל!
export const ThemeContext = React.createContext({
  theme: themes.dark,
  toggleTheme: () => {},
});

theme-toggler-button.js

import {ThemeContext} from './theme-context';

function ThemeTogglerButton() {
  // מקבל לא רק את ערכת הנושא Theme Toggler-כפתור ה
  // מהקונטקסט כדי לעדכן את ערכה toggleTheme אלא גם את הפונקציה
  return (
    <ThemeContext.Consumer>
      {({theme, toggleTheme}) => (
        <button
          onClick={toggleTheme}
          style={{backgroundColor: theme.background}}>
          Toggle Theme
        </button>
      )}
    </ThemeContext.Consumer>
  );
}

export default ThemeTogglerButton;

app.js

import {ThemeContext, themes} from './theme-context';
import ThemeTogglerButton from './theme-toggler-button';

class App extends React.Component {
  constructor(props) {
    super(props);

    this.toggleTheme = () => {
      this.setState(state => ({
        theme:
          state.theme === themes.dark
            ? themes.light
            : themes.dark,
      }));
    };

    // מכיל גם את פונקצית העדכון state-ה
    // שתועבר לספק הקונטקסט
    this.state = {
      theme: themes.light,
      toggleTheme: this.toggleTheme,
    };
  }

  render() {
    // בשלמותו מועבר לספק state-ה
    return (
      <ThemeContext.Provider value={this.state}>
        <Content />
      </ThemeContext.Provider>
    );
  }
}

function Content() {
  return (
    <div>
      <ThemeTogglerButton />
    </div>
  );
}

ReactDOM.render(<App />, document.root);

שימוש ביותר מקונטקסט אחד

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

// קונטקסט ערכת הנושא עם ברירת מחדל בהירה
const ThemeContext = React.createContext('light');

// קונטקסט המשתמש המאומת
const UserContext = React.createContext({
  name: 'Guest',
});

class App extends React.Component {
  render() {
    const {signedInUser, theme} = this.props;

    // קומפוננטה שמספקת ערכי ברירת מחדל לקונטקסט
    return (
      <ThemeContext.Provider value={theme}>
        <UserContext.Provider value={signedInUser}>
          <Layout />
        </UserContext.Provider>
      </ThemeContext.Provider>
    );
  }
}

function Layout() {
  return (
    <div>
      <Sidebar />
      <Content />
    </div>
  );
}

// קומפוננטה יכולה לצרוך יותר מקונטקסט אחד
function Content() {
  return (
    <ThemeContext.Consumer>
      {theme => (
        <UserContext.Consumer>
          {user => (
            <ProfilePage user={user} theme={theme} />
          )}
        </UserContext.Consumer>
      )}
    </ThemeContext.Consumer>
  );
}

אם שני ערכי קונטקסט (או יותר) בדרך כלל משומשים ביחד, יכול להיות שתרצו לשקול יצירת קומפוננטת רינדור prop שתספק אותם ביחד.

הסתיגויות

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

class App extends React.Component {
  render() {
    return (
      <Provider value={{something: 'something'}}>
        <Toolbar />
      </Provider>
    );
  }
}

כדי לעקוף את הבעיה הזאת, אפשר להעביר את הערך ל-state של האב:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: {something: 'something'},
    };
  }

  render() {
    return (
      <Provider value={this.state.value}>
        <Toolbar />
      </Provider>
    );
  }
}

ממשק תכנות מדור קודם

הערה

בעבר, React הוציאה ממשק תכנות נסיוני לקונטקסט. הממשק הישן ייתמך בכל גרסאות ה-16.x, אבל אפליקציות שמשתמשות בו צריכות לעבור לשימוש בגרסה החדשה. הממשק הישן יוסר בגרסה הראשית הבאה של React. עוד מידע על ממשק הקונטקסט הישן.