Un peu de pratique avec JS Proxy pour optimiser le rafraîchissement des composants React lors de l'utilisation de useContext

Le problème que nous résolvons

Le contexte dans react peut contenir de nombreuses valeurs et différents consommateurs du contexte ne peuvent utiliser qu'une partie des valeurs. Cependant, lorsqu'une valeur change par rapport au contexte, tous les consommateurs (en particulier, tous les composants qui l'utilisent useContext



) seront renvoyés , même s'ils ne dépendent pas de la partie modifiée des données. Le problème est assez discuté et a de nombreuses solutions différentes. En voici quelques-uns. J'ai créé cet exemple pour illustrer le problème. Ouvrez simplement la console et appuyez sur les boutons.





but

Notre solution devrait modifier au minimum les bases de code existantes. Je souhaite créer mon propre hook personnalisé useSmartContext



 avec la même signature que celle de useContext



, mais qui ne rendra le composant que lorsque la partie utilisée du contexte change.





Idée

Découvrez ce qui est utilisé par le composant en encapsulant la useSmartContext



valeur de retour dans un proxy.





Mise en œuvre

Étape 1.





Nous créons notre propre crochet.





const useSmartContext(context) {
  const usedFieldsRef = useRef(new Set());

  const proxyRef = useRef(
    new Proxy(
      {},
      {
        get(target, prop) {
          usedPropsRef.current.add(prop);
          return context._currentValue[prop];
        }
      }
    )
  );

  return proxyRef.current;
}
      
      



Nous avons créé une liste dans laquelle nous stockerons les champs de contexte utilisés. Nous avons créé un proxy avec un get



 piège dans lequel nous remplissons cette liste. Target



cela n'a pas d'importance pour nous, alors j'ai passé un objet vide comme premier argument {}



.





Étape 2.





Vous devez obtenir la valeur du contexte lors de sa mise à jour et comparer la valeur des champs de la liste usedPropsRef



avec les valeurs précédentes. Si quelque chose a changé, déclenchez un nouveau rendu. useContext



Nous ne pouvons pas l' utiliser dans notre hook, sinon notre hook commencera également à provoquer un nouveau rendu pour toutes les modifications. Ici commencent les danses avec un tambourin. J'espérais à l'origine m'abonner aux changements de contexte avec context.Consumer



. A savoir comme ceci:





React.createElement(context.Consumer, {}, (newContextVakue) => {/* handle */})
      
      



. . - , , , .





React



, useContext



. , , , . - . _currentValue



. , undefined



. ! Proxy , . Object.defineProperty



.






  let val = context._currentValue;
  let notEmptyVal = context._currentValue;
  Object.defineProperty(context, "_currentValue", {
    get() {
      return val;
    },
    set(newVal) {
      if (newVal) {
        //     !
      }
      val = newVal;
    }
  });
      
      



! : useSmartContext



  Object.defineProperty



  . useSmartContext



  createContext



.





export const createListenableContext = () => {
  const context = createContext();

  const listeners = [];
  let val = context._currentValue;
  let notEmptyVal = context._currentValue;
  Object.defineProperty(context, "_currentValue", {
    get() {
      return val;
    },
    set(newVal) {
      if (newVal) {
        listeners.forEach((cb) => cb(notEmptyVal, newVal));
        notEmptyVal = newVal;
      }
      val = newVal;
    }
  });

  context.addListener = (cb) => {
    listeners.push(cb);

    return () => listeners.splice(listeners.indexOf(cb), 1);
  };

  return context;
};
      
      



, . ,





const useSmartContext = (context) => {
  const usedFieldsRef = useRef(new Set());
  useEffect(() => {
    const clear = context.addListener((prevValue, newValue) => {
      let isChanged = false;
      usedFieldsRef.current.forEach((usedProp) => {
        if (!prevValue || newValue[usedProp] !== prevValue[usedProp]) {
          isChanged = true;
        }
      });

      if (isChanged) {
        //  
      }
    });

    return clear;
  }, [context]);

  const proxyRef = useRef(
    new Proxy(
      {},
      {
        get(target, prop) {
          usedFieldsRef.current.add(prop);
          return context._currentValue[prop];
        }
      }
    )
  );

  return proxyRef.current;
};

      
      



3.





. useState



, . , . - ?





// ...
const [, rerender] = useState();
const renderTriggerRef = useRef(true);
// ...  
if (isChanged) {
  renderTriggerRef.current = !renderTriggerRef.current;
  rerender(renderTriggerRef.current);
}
      
      



, . . useContext



->useSmartContext



createContext



->createListenableContext



.





, !





  • ,





  • Monkey patch





















, . .





En écrivant cet article, je suis tombé sur une autre bibliothèque qui résout le même problème avec l'optimisation des redessins lors de l'utilisation du contexte. La solution de cette bibliothèque, à mon avis, est la plus correcte que j'ai vue. Ses sources sont beaucoup plus lisibles et elles m'ont donné quelques idées sur la façon de préparer notre exemple de production sans changer le mode d'utilisation. Si je rencontre une réponse positive de votre part, j'écrirai sur la nouvelle implémentation.





Merci à tous pour votre attention.








All Articles