Fermer

août 18, 2022

Une introduction à la propagation de contexte en JavaScript

Une introduction à la propagation de contexte en JavaScript


Résumé rapide ↬
React a popularisé l’idée de la propagation du contexte au sein de nos applications avec son API de contexte, une alternative au prop-drilling et à la synchronisation des états dans différentes parties des applications. Cet article apporte une brève introduction à la propagation de contexte en JavaScript et montre qu’il n’y a aucune magie derrière certaines des API React les plus utiles.

React a popularisé l’idée de propagation de contexte au sein de nos applications avec son API de contexte. Dans le monde de React, le contexte est utilisé comme une alternative à l’exploration d’accessoires et à la synchronisation de l’état dans différentes parties des applications.

« Context fournit un moyen de transmettre des données via l’arborescence des composants sans avoir à transmettre manuellement les accessoires à tous les niveaux. »

— Réagissez aux documents

Vous pouvez imaginer le contexte de React comme une sorte de « trou de ver » par lequel vous pouvez transmettre des valeurs quelque part dans votre arborescence de composants et y accéder plus bas dans les composants de vos enfants.

L’extrait de code suivant est un exemple plutôt simpliste (et assez inutile) de l’API de contexte de React, mais il montre comment nous pouvons utiliser des valeurs définies plus haut dans l’arborescence des composants sans les transmettre explicitement aux composants enfants.

Dans l’extrait ci-dessous, nous avons notre application qui a un Color composant en elle. Ce composant Color affiche un message contenant le message défini dans son composant parent – l’application, uniquement sans qu’il soit transmis directement comme accessoire au composant, mais plutôt – en le faisant apparaître « magiquement » grâce à l’utilisation de useContext.

import {createContext, useContext} from 'react'

const MyContext = createContext();

function App() {
  return (
    <MyContext.Provider value={color: "red"} >
      <Color/ >
    </MyContext.Provider >
  );
}

function Color() {
  const {color} = useContext(MyContext);

  return <span >Your color is: {color}</span >
}

Bien que le cas d’utilisation de la propagation de contexte soit clair lors de la création d’applications orientées utilisateur avec un framework d’interface utilisateur, le besoin d’une API similaire existe même lorsque vous n’utilisez pas du tout de framework d’interface utilisateur ou même lorsque vous ne créez pas d’interface utilisateur.

Pourquoi devrions-nous nous en soucier ?

À mes yeux, il y a deux raisons d’essayer de le mettre en œuvre.

Premièrement, en tant qu’utilisateur d’un framework, il est très important de comprendre comment il fait les choses. Nous considérons souvent les outils que nous utilisons comme « magiques » et les choses qui fonctionnent. Essayer d’en construire des parties pour vous-même le démystifie et vous aide à voir qu’il n’y a pas de magie impliquée et que sous le capot, les choses peuvent être assez simples.

Deuxièmement, l’API de contexte peut également être utile lorsque vous travaillez sur des applications non UI.

Chaque fois que nous construisons une sorte de support à une grande application, nous sommes confrontés à des fonctions qui s’appellent les unes les autres, et la pile d’appels peut s’étendre sur plusieurs couches. Le fait de devoir transmettre des arguments plus bas peut créer beaucoup de désordre, surtout si vous n’utilisez pas toutes ces variables à tous les niveaux. Dans le monde de React, nous l’appelons « prop drill ».

Alternativement, si vous êtes un auteur de bibliothèque et que vous comptez sur des rappels qui vous sont transmis par le consommateur, vous pouvez avoir des variables déclarées à différents niveaux de votre environnement d’exécution et vous souhaitez qu’elles soient disponibles plus bas. Prenons l’exemple d’un framework de tests unitaires.

describe('add', () => {
  it('Should add two numbers', () => {
    expect(add(1, 1)).toBe(2);
  });
});

Dans l’exemple suivant, nous avons cette structure :

  1. describe est appelée et appelle la fonction de rappel qui lui est transmise.
  2. dans le rappel, nous avons un it appel.

Que voulons-nous faire ?

Écrivons maintenant l’implémentation de base de notre framework de tests unitaires. J’adopte l’approche très naïve et heureuse pour rendre le code aussi simple que possible, mais ceci, bien sûr, n’est pas quelque chose que vous devriez utiliser dans la vraie vie.

function describe(description, callback) {
  callback()
}

function it(text, callback) {
  try {
    callback()
    console.log("✅ " + text)
} catch {
    console.log("🚨 " + text)
  }
}

Dans l’exemple ci-dessus, nous avons la fonction « describe » qui appelle son callback. Ce rappel peut contenir différents appels à « it ». « it », à son tour, enregistre si le test a réussi ou échoué.

Supposons que, parallèlement au message de test, nous souhaitions également consigner le message de « describe » :

describe('calculator: Add', () => {
  it("Should correctly add two numbers", () => {
    expect(add(1, 1)).toBe(2);
  });
});

Consignerait à la console le message de test précédé de la description :

"calculator: Add > ✅ Should correctly add two numbers"

Pour ce faire, nous devons d’une manière ou d’une autre faire en sorte que le message de description « saute par-dessus » le code utilisateur et, d’une manière ou d’une autre, trouve son chemin dans l’implémentation de la fonction « it ».

Quelles solutions pourrions-nous essayer ?

Lorsque nous essayons de résoudre ce problème, nous pouvons essayer plusieurs approches. Je vais essayer d’en passer quelques-unes en revue et de montrer pourquoi elles pourraient ne pas convenir à notre scénario.

  • Utiliser « ceci »
    Nous pourrions essayer d’instancier une classe et de propager les données à travers « ceci », mais il y a deux problèmes ici. « ceci » est très pointilleux. Cela ne fonctionne pas toujours comme prévu, en particulier lors de la factorisation des fonctions fléchées, qui utilisent la portée lexicale pour déterminer la valeur « this » actuelle, ce qui signifie que nos consommateurs devront utiliser le mot-clé de la fonction. Parallèlement à cela, il n’y a pas de relation entre « tester » et « décrire », il n’y a donc aucun moyen réel de partager l’instance actuelle.
  • Émission d’un événement
    Pour émettre un événement, nous avons besoin de quelqu’un pour l’attraper. Mais que se passe-t-il si nous avons plusieurs suites en cours d’exécution en même temps ? Étant donné que nous n’avons aucune relation entre les appels de test et leurs « descriptions » respectées, qu’est-ce qui empêcherait les autres suites de capter également leurs événements ?
  • Stocker le message sur un objet global
    Les objets globaux souffrent des mêmes problèmes que l’émission d’un événement, et aussi, nous polluons la portée globale. Avoir un objet global signifie également que notre valeur de contexte peut être inspectée et même modifiée de l’extérieur de l’exécution de notre fonction, ce qui peut être très risqué.
  • Lancer une erreur
    Cela peut techniquement fonctionner : notre « description » peut détecter les erreurs générées par « cela », mais cela signifie qu’au premier échec, nous arrêterons l’exécution et aucun autre test ne pourra s’exécuter.
Plus après saut! Continuez à lire ci-dessous ↓

Contexte à la rescousse !

À présent, vous devez avoir deviné que je préconise une solution qui serait quelque peu similaire dans sa conception à la propre API contextuelle de React, et je pense que notre exemple de test unitaire de base pourrait être un bon candidat pour le tester.

L’anatomie du contexte

Décomposons quelles sont les parties qui composent le contexte de React :

  1. React.createContext — crée un nouveau contexte, définit essentiellement un nouveau conteneur spécialisé pour nous.
  2. Fournisseur — la valeur de retour createContext. Il s’agit d’un objet avec la propriété « provider ». La propriété provider est un composant en soi, et lorsqu’elle est utilisée dans une application React, c’est l’entrée de notre « trou de ver ».
  3. React.useContext – une fonction qui, lorsqu’elle est appelée dans un arbre React enveloppé d’un contexte, sert de point de sortie de notre trou de ver et permet d’en extraire des valeurs.

Jetons un coup d’œil au propre contexte de React :

Une capture d'écran du codage
L’objet contextuel React (Grand aperçu)

Il semble que l’objet contextuel React soit assez complexe. Il contient un fournisseur et un consommateur qui sont en fait des éléments React. Gardons cette structure à l’esprit pour l’avenir.

Sachant ce que nous savons maintenant du contexte de React, essayons de réfléchir à la manière dont ses différentes parties devraient interagir avec notre exemple de test unitaire. Je vais faire un scénario absurde juste pour que nous puissions imaginer les différents composants fonctionnant dans la vraie vie.

const TestContext = createContext()

function describe(description, callback) {
  // <TestContext.Provider value={{description}} >
callback()
  // </TestContext.Provider >
}

function it(text, callback) {
  // const { description } = useContext(TestContext);

  try {
    callback()
    console.log(description + " > ✅ " + text)
  } catch {
    console.log(description+ " > 🚨 " + text)
  }
}

Mais clairement, cela ne peut pas fonctionner. Tout d’abord, nous ne pouvons pas utiliser React Elements dans notre code JS vanille. Deuxièmement, nous ne pouvons pas utiliser le contexte de React en dehors de React. Droit? Droit.

Alors adaptons cette structure en vrai JS :

const TestContext = createContext()

function describe(description, callback) {

  TestContext.Provider({description}, () => {
callback()
  });
}

function it(text, callback) {
  const { description } = useContext(TestContext);

  try {
    callback()
    console.log(description + " > ✅ " + text)
  } catch {
    console.log(description+ " > 🚨 " + text)
  }
}

OK, donc ça commence à ressembler plus à JavaScript. Qu’avons-nous ici ?

Eh bien, la plupart du temps – au lieu de notre composant ContextProvider, nous utilisons TextContext.Provider, qui prend un objet avec les références à nos valeurs, et useContext() qui nous sert de portail – afin que nous puissions puiser dans notre trou de ver.

Cela peut-il fonctionner, cependant? Essayons.

Rédaction de notre API

Maintenant que nous avons le concept général de la façon dont nous allons utiliser notre contexte, commençons par définir les fonctions que nous allons exposer. Puisque nous savons déjà à quoi ressemble l’API React Context, nous pouvons nous baser sur cela.

function createContext() {
  return {
    Provider,
    Consumer
  }

  function Provider(value, callback) {}

  function Consumer() {}
}

function useContext(ctxRef) {}

Nous définissons deux fonctions, tout comme React. createContext et useContext. createContext renvoie un fournisseur et un consommateur, tout comme le contexte de React, et useContext prend une référence de contexte.

Quelques concepts à connaître avant de plonger

Ce que nous allons faire à partir de maintenant s’appuiera sur deux idées fondamentales qui sont importantes pour les développeurs JavaScript. Je ne vais pas les expliquer ici, mais si vous vous sentez fragile à propos de ces sujets, vous êtes plus qu’encouragé à les lire :

  1. Fermetures JavaScript
    De MDN: « UN fermeture est la combinaison d’une fonction regroupée (enfermée) avec des références à son état environnant (le environnement lexical). En d’autres termes, une fermeture vous donne accès à la portée d’une fonction externe à partir d’une fonction interne. En JavaScript, les fermetures sont créées chaque fois qu’une fonction est créée, au moment de la création de la fonction.
  2. La nature synchrone de JavaScript À la base, Javascript est synchrone et bloquant. Oui, il a des promesses asynchrones, des rappels et async/attente – et ils nécessiteront une gestion spéciale, mais pour la plupart, traitons JavaScript comme synchrone, car à moins que nous n’atteignions ces domaines, ou des implémentations de navigateur TRÈS bizarres, Le code JavaScript est synchrone.

Ces deux idées apparemment sans rapport sont ce qui permet à notre contexte de fonctionner. L’hypothèse est que, si nous fixons une valeur dans Provider et appelez notre rappel, nos valeurs resteront et seront disponibles tout au long de notre exécution de fonction synchrone. Nous avons juste besoin d’un moyen d’y accéder. C’est ce que useContext est pour.

Stocker les valeurs dans notre contexte

Le contexte est utilisé pour propager les données dans notre pile d’appels, donc la première chose que nous voulons faire est de stocker des informations dessus. Définissons un contextValue variable au sein de notre createContext fonction. Résidant dans la fermeture de createContextgarantit que toutes les fonctions définies dans createContext y auront accès même plus tard.

function createContext() {
  let contextValue = undefined;

  function Provider(value, callback) {}

  function Consumer() {}

  return {
    Provider,
    Consumer
  }
}

Maintenant que nous avons la valeur stockée dans le contexte, notre Provider fonction peut stocker la valeur qu’elle accepte, et la Consumer fonction peut le renvoyer.

function createContext() {
  let contextValue = undefined;

  function Provider(value, callback) {
    contextValue = value;
  }

  function Consumer() {
    return contextValue;
  }

  return {
    Provider,
    Consumer
  }
}

Pour accéder aux données depuis notre fonction, nous pouvons simplement appeler notre fonction Consumer, mais juste pour que notre interface fonctionne exactement comme celle de React, faisons également en sorte que useContext ait accès aux données.

function useContext(ctxRef) {
  return ctxRef.Consumer();
}

Appel de nos rappels

Maintenant, la partie amusante commence. Comme mentionné, cette méthode repose sur la nature synchrone de JavaScript. Cela signifie qu’à partir du moment où nous exécutons notre rappel, nous connaîtreavec certitude, qu’aucun autre code ne s’exécutera — ce qui signifie que nous ne vraiment devons protéger notre contexte contre les modifications pendant notre exécution, mais à la place, nous n’avons besoin de le nettoyer qu’immédiatement après l’exécution de notre rappel.

function createContext() {
  let contextValue = undefined;

  function Provider(value, callback) {
    contextValue = value;
    callback();
    contextValue = undefined;
  }

  function Consumer() {
    return contextValue;
  }

  return {
    Provider,
    Consumer
  }
}

C’est tout ce qu’on peut en dire. Vraiment. Si notre fonction est appelée avec la fonction Provider, tout au long de son exécution elle aura accès à la valeur Provider.

Et si nous avions un contexte imbriqué ?

L’imbrication des contextes est quelque chose qui peut arriver. Par exemple, quand j’ai un describe dans un describe. Dans un tel cas, notre contexte se brisera lors de la sortie du contexte le plus interne, car après chaque exécution de rappel, nous réinitialisons la valeur de contexte sur undefined, et puisque les deux couches du contexte partagent la même fermeture – le fournisseur le plus interne sera réinitialisé la valeur des calques au-dessus.

function Provider(value, callback) {
  contextValue = value;
  callback();
  contextValue = undefined;
}

Heureusement, il est très facile à manipuler. Lorsque nous entrons dans un contexte, il nous suffit de sauvegarder sa valeur actuelle dans une variable et de la remettre à sa valeur lorsque nous quittons le contexte :

function Provider(value, callback) {
  let currentValue = contextValue;
  contextValue = value;
  callback();
  contextValue = currentValue;
}

Maintenant, chaque fois que nous sortons du contexte, il reviendra à la valeur précédente, et s’il n’y a plus de couches de contexte au-dessus, nous reviendrons à la valeur initiale – qui n’est pas définie.

Une autre fonctionnalité que nous n’avons pas implémentée aujourd’hui est la valeur par défaut du contexte. Dans React, vous pouvez initialiser le contexte avec une valeur par défaut qui sera renvoyée par le Consumer/useContext si nous ne sommes pas dans un contexte en cours d’exécution.

Si vous en êtes arrivé là, vous avez toutes les connaissances et tous les outils pour essayer de le mettre en œuvre par vous-même – j’aimerais voir ce que vous proposez.

Est-ce utilisé quelque part ?

Oui! en fait j’ai construit le package de contexte sur NPM qui fait exactement cela, avec quelques modifications et un tas de fonctionnalités supplémentaires – y compris la prise en charge complète du dactylographie, la fusion des contextes imbriqués, les valeurs de retour de la fonction « Provider », les valeurs initiales du contexte et même le middleware d’enregistrement de contexte.

Vous pouvez inspecter le code source complet du package ici : https://github.com/ealush/vest/blob/latest/packages/context/src/context.ts

Et il est largement utilisé à l’intérieur Cadre de validation des gilets, un framework de validation de formulaires inspiré des bibliothèques de tests unitaires telles que Mocha ou Jest. Le contexte sert de moteur d’exécution principal de Vest, comme on peut le voir ici.

J’espère que vous avez apprécié cette brève introduction à la propagation de contexte en JavaScript et qu’elle vous a montré qu’il n’y a aucune magie derrière certaines des API React les plus utiles.

Lectures complémentaires sur Smashing Magazine

Éditorial fracassant(nl, il)




Source link