Contexte

Le Contexte offre un moyen de faire passer des données à travers l’arborescence du composant sans avoir à passer manuellement les props à chaque niveau.

Dans une application React typique, les données sont passées de haut en bas (du parent à l’enfant) via les props, mais cela peut devenir lourd pour certains types de props (ex. les préférences régionales, le thème de l’interface utilisateur) qui s’avèrent nécessaires pour de nombreux composants au sein d’une application. Le Contexte offre un moyen de partager des valeurs comme celles-ci entre des composants sans avoir à explicitement passer une prop à chaque niveau de l’arborescence.

Quand utiliser le Contexte

Le Contexte est conçu pour partager des données qui peuvent être considérées comme « globales » pour une arborescence de composants React, comme l’utilisateur actuellement authentifié, le thème, ou la préférence de langue. Par exemple, dans le code ci-dessous nous faisons passer manuellement la prop theme afin de styler le composant Button :

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

function Toolbar(props) {
  // Le composant Toolbar doit prendre une prop supplémentaire `theme` et la
  // passer au ThemedButton. Ça peut devenir pénible si chaque bouton de l’appli
  // a besoin de connaître le thème parce qu’il faudra le faire passer à travers
  // tous les composants.
  return (
    <div>
      <ThemedButton theme={props.theme} />
    </div>
  );
}

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

En utilisant le Contexte, nous pouvons éviter de passer les props à travers des éléments intermédiaires :

// Le Contexte nous permet de transmettre une prop profondément dans l’arbre des
// composants sans la faire passer explicitement à travers tous les composants.
// Crée un contexte pour le thème (avec “light” comme valeur par défaut).
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    // Utilise un Provider pour passer le thème plus bas dans l’arbre.
    // N’importe quel composant peut le lire, quelle que soit sa profondeur.
    // Dans cet exemple, nous passons “dark” comme valeur actuelle.
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// Un composant au milieu n’a plus à transmettre explicitement le thème
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // Définit un contextType pour lire le contexte de thème actuel.  React va
  // trouver le Provider de thème ancêtre le plus proche et utiliser sa valeur.
  // Dans cet exemple, le thème actuel est “dark”.
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}

Avant d’utiliser le Contexte

Le Contexte est principalement utilisé quand certaines données doivent être accessibles par de nombreux composants à différents niveaux d’imbrication. Utilisez-le avec parcimonie car il rend la réutilisation des composants plus difficile.

Si vous voulez seulement éviter de passer certaines props à travers de nombreux niveaux, la composition des composants est souvent plus simple que le contexte.

Par exemple, prenez un composant Page qui passe des props user et avatarSize plusieurs niveaux plus bas pour que les composants profondément imbriqués Link et Avatar puissent les lire :

<Page user={user} avatarSize={avatarSize} />
// ... qui affiche ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... qui affiche ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ... qui affiche ...
<Link href={user.permalink}>
  <Avatar user={user} size={avatarSize} />
</Link>

Ça peut paraître redondant de passer les props user et avatarSize à travers plusieurs niveaux, si au final seul le composant Avatar en a réellement besoin. Il est également pénible qu’à chaque fois que le composant Avatar a besoin de davantage de props d’en haut, vous ayez à les ajouter à tous les niveaux.

Un des moyens de résoudre ce problème sans le contexte consisterait à transmettre le composant Avatar lui-même de façon à ce que les composants intermédiaires n’aient pas besoin de connaître les props user ou 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} />;
}

// À présent nous avons :
<Page user={user} avatarSize={avatarSize} />
// ... qui affiche ...
<PageLayout userLink={...} />
// ... qui affiche ...
<NavigationBar userLink={...} />
// ... qui affiche ...
{props.userLink}

Avec cette modification, seulement le composant le plus haut placé, Page, a besoin de connaître l’utilisation de user et avatarSize par les composants Link et Avatar.

Cette inversion de contrôle peut rendre votre code plus propre dans de nombreux cas en réduisant le nombre de props que vous avez besoin de passer à travers votre application et vous donne plus de contrôle sur les composants racines. Cependant, ce n’est pas toujours la bonne approche : déplacer la complexité vers le haut de l’arborescence rend les composants des niveaux supérieurs plus compliqués et force les composants de plus bas niveau à être plus flexibles que vous pourriez le souhaiter.

Vous n’êtes pas limité·e à un unique enfant pour un composant. Vous pouvez passer plusieurs enfants, ou même prévoir dans votre JSX plusieurs emplacements séparés pour les enfants comme documenté ici :

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}
    />
  );
}

Ce motif est suffisant pour les nombreux cas où vous avez besoin de découpler un enfant de ses parents directs. Vous pouvez aller encore plus loin avec les props de rendu si l’enfant a besoin de communiquer avec le parent avant de s’afficher.

Cependant, parfois les mêmes données ont besoin d’être accessibles par de nombreux composants dans l’arborescence, et à différents niveaux d’imbrication. Le Contexte vous permet de « diffuser » ces données, et leurs mises à jour, à tous les composants plus bas dans l’arbre. Les exemples courants où l’utilisation du Contexte apporte une simplification incluent la gestion des préférences régionales, du thème ou d’un cache de données.

API

React.createContext

const MyContext = React.createContext(defaultValue);

Crée un objet Context. Lorsque React affiche un composant qui s’abonne à cet objet Context, il lira la valeur actuelle du contexte depuis le Provider le plus proche situé plus haut dans l’arborescence.

L’argument defaultValue est uniquement utilisé lorsqu’un composant n’a pas de Provider correspondant au-dessus de lui dans l’arborescence. Ça peut être utile pour tester des composants de manière isolée sans les enrober. Remarquez que passer undefined comme valeur au Provider n’aboutit pas à ce que les composants consommateurs utilisent defaultValue.

Context.Provider

<MyContext.Provider value={/* une valeur */}>

Chaque objet Contexte est livré avec un composant React Provider qui permet aux composants consommateurs de s’abonner aux mises à jour du contexte.

Il accepte une prop value à transmettre aux composants consommateurs descendants de ce Provider(plus bas dans l’arbre, donc). Un Provider peut être connecté à plusieurs consommateurs. Les Provider peuvent être imbriqués pour remplacer leur valeur plus profondément dans l’arbre.

Tous les consommateurs qui sont descendants d’un Provider se rafraîchiront lorsque la prop value du Provider change. La propagation du Provider vers ses consommateurs descendants n’est pas assujettie à la méthode shouldComponentUpdate, de sorte que le consommateur est mis à jour même lorsqu’un composant ancêtre abandonne sa mise à jour.

On détermine si modification il y a en comparant les nouvelles et les anciennes valeurs avec le même algorithme que Object.is.

Remarque

La manière dont les modifications sont déterminées peut provoquer des problèmes lorsqu’on passe des objets dans value : voir les limitations.

Class.contextType

class MyClass extends React.Component {
  componentDidMount() {
    let value = this.context;
    /* produit un effet de bord au montage sur la valeur de MyContext */
  }
  componentDidUpdate() {
    let value = this.context;
    /* ... */
  }
  componentWillUnmount() {
    let value = this.context;
    /* ... */
  }
  render() {
    let value = this.context;
    /* affiche quelque chose basé sur la valeur de MyContext */
  }
}
MyClass.contextType = MyContext;

La propriété contextType d’une classe peut recevoir un objet Contexte créé par React.createContext(). Ça vous permet de consommer la valeur la plus proche de ce Contexte en utilisant this.context. Vous pouvez la référencer dans toutes les méthodes de cycle de vie, y compris la fonction de rendu.

Remarque

Vous pouvez vous abonner à un unique contexte en utilisant cette API. Si vous voulez lire plus d’un contexte, voyez Consommer plusieurs contextes.

Si vous utilisez la syntaxe expérimentale des champs publics de classe, vous pouvez utiliser un champ statique de classe pour initialiser votre contextType.

class MyClass extends React.Component {
  static contextType = MyContext;
  render() {
    let value = this.context;
    /* Affiche quelque chose basé sur la valeur */
  }
}

Context.Consumer

<MyContext.Consumer>
  {value => /* affiche quelque chose basé sur la valeur du contexte */}
</MyContext.Consumer>

Un composant React qui s’abonne aux modifications de contexte. Ça permet de s’abonner à un contexte au sein d’une fonction composant.

Nécessite une fonction enfant. La fonction reçoit le contexte actuel et renvoie un nœud React. L’argument value envoyé à la fonction sera égal à la prop value du Provider le plus proche (plus haut dans l’arbre) pour le contexte en question. Si il n’y pas de Provider pour le contexte voulu, l’argument value sera égal à la defaultValue passée lors de son createContext().

Remarque

Pour en apprendre davantage sur l’approche « fonction enfant », voyez les props de rendu.

Exemples

Contexte dynamique

Un exemple plus complexe avec des valeurs dynamiques pour le thème :

theme-context.js

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

export const ThemeContext = React.createContext(
  themes.dark // valeur par défaut
);

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';

// Un composant intermédiaire qui utilise ThemedButton
function Toolbar(props) {
  return (
    <ThemedButton onClick={props.changeTheme}>
      Changer le thème
    </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() {
    // Le bouton ThemedButton à l'intérieur du ThemeProvider
    // utilise le thème de l’état local tandis que celui à l'extérieur
    // utilise le thème dark par défaut
    return (
      <Page>
        <ThemeContext.Provider value={this.state.theme}>
          <Toolbar changeTheme={this.toggleTheme} />
        </ThemeContext.Provider>
        <Section>
          <ThemedButton />
        </Section>
      </Page>
    );
  }
}

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

Mettre à jour le Contexte à partir d’un composant imbriqué

Il est souvent nécessaire de mettre à jour le contexte à partir d’un composant imbriqué profondément dans l’arbre des composants. Dans un tel cas, vous pouvez passer une fonction à travers le contexte qui permet aux consommateurs de le mettre à jour :

theme-context.js

// Assurez-vous que la forme de la valeur par défaut passée à
// createContext correspond à la forme que les consommateurs attendent !
export const ThemeContext = React.createContext({
  theme: themes.dark,
  toggleTheme: () => {},
});

theme-toggler-button.js

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

function ThemeTogglerButton() {
  // Le Theme Toggler Button reçoit non seulement le thème
  // mais aussi une fonction toggleTheme du contexte
  return (
    <ThemeContext.Consumer>
      {({theme, toggleTheme}) => (
        <button
          onClick={toggleTheme}
          style={{backgroundColor: theme.background}}>
          Changer le thème
        </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,
      }));
    };

    // L’état local contient aussi la fonction de mise à jour donc elle va
    // être passée au fournisseur de contexte
    this.state = {
      theme: themes.light,
      toggleTheme: this.toggleTheme,
    };
  }

  render() {
    // L’état local entier est passé au fournisseur
    return (
      <ThemeContext.Provider value={this.state}>
        <Content />
      </ThemeContext.Provider>
    );
  }
}

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

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

Consommer plusieurs Contextes

Pour conserver un rafraîchissement rapide du contexte, React a besoin que chaque consommateur de contexte soit un nœud à part dans l’arborescence.

// Contexte de thème, clair par défaut
const ThemeContext = React.createContext('light');

// Contexte d’utilisateur authentifié
const UserContext = React.createContext({
  name: 'Invité',
});

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

    // Le composant App qui donne accès aux différentes valeurs des contextes
    return (
      <ThemeContext.Provider value={theme}>
        <UserContext.Provider value={signedInUser}>
          <Layout />
        </UserContext.Provider>
      </ThemeContext.Provider>
    );
  }
}

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

// Un composant peut consommer plusieurs contextes
function Content() {
  return (
    <ThemeContext.Consumer>
      {theme => (
        <UserContext.Consumer>
          {user => (
            <ProfilePage user={user} theme={theme} />
          )}
        </UserContext.Consumer>
      )}
    </ThemeContext.Consumer>
  );
}

Si plusieurs valeurs de contexte sont souvent utilisées ensemble, vous voudrez peut-être créer votre propre composant avec prop de rendu qui fournira les deux.

Limitations

Dans la mesure où le contexte utilise une identité référentielle pour déterminer quand se rafraîchir, il y a des cas piégeux qui peuvent déclencher des rafraîchissements involontaires pour les consommateurs lorsque le parent d’un fournisseur se rafraîchit. Par exemple, le code ci-dessous va rafraîchir chaque consommateur, le Provider se rafraîchissant lui-même parce qu’un nouvel objet est créé à chaque fois pour value :

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

Pour contourner ce problème, placez la valeur dans l’état du parent :

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

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

API historique

Remarque

React fournissait auparavant une API de contextes expérimentale. L’ancienne API restera prise en charge par toutes les versions 16.x, mais les applications qui l’utilisent devraient migrer vers la nouvelle version. L’API historique sera supprimée dans une future version majeure de React. Lisez la documentation sur l’API historique de contexte ici.