Un poco de práctica con JS Proxy para optimizar el rediseño de los componentes de React cuando se usa useContext

El problema que estamos resolviendo

El contexto en react puede contener muchos valores y diferentes consumidores del contexto pueden usar solo una parte de los valores. Sin embargo, cuando cualquier valor cambia del contexto, todos los consumidores (en particular, todos los componentes que utilizan useContext



) se volverán a generar , incluso si no dependen de la parte modificada de los datos. El problema está bastante discutido y tiene muchas soluciones diferentes. Éstos son algunos de ellos. Creé este ejemplo para demostrar el problema. Simplemente abra la consola y presione los botones.





objetivo

Nuestra solución debería cambiar las bases de código existentes al mínimo. Quiero crear mi propio gancho personalizado useSmartContext



 con la misma firma que la de useContext



, pero que solo volverá a representar el componente cuando cambie la parte utilizada del contexto.





Idea

Descubra qué está siendo utilizado por el componente envolviendo el useSmartContext



valor de retorno en un Proxy.





Implementación

Paso 1.





Creamos nuestro propio anzuelo.





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



Hemos creado una lista en la que almacenaremos los campos de contexto utilizados. Creamos un proxy con una get



 trampa en la que llenamos esta lista. Target



no nos importa, así que pasé un objeto vacío como primer argumento {}



.





Paso 2.





Debe obtener el valor del contexto cuando se actualiza y comparar el valor de los campos de la lista usedPropsRef



con los valores anteriores. Si algo ha cambiado, active una nueva renderización. useContext



No podemos usarlo dentro de nuestro gancho, de lo contrario, nuestro gancho también comenzará a generar una nueva representación para todos los cambios. Aquí comienzan los bailes con pandereta. Originalmente esperaba suscribirme a los cambios de contexto con context.Consumer



. Es decir, así:





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





















, . .





Mientras escribía este artículo, me encontré con otra biblioteca que resuelve el mismo problema con la optimización de los redibujos cuando se usa el contexto. La solución de esta biblioteca, en mi opinión, es la más correcta que he visto. Sus fuentes son mucho más legibles y me dieron un par de ideas sobre cómo preparar nuestra producción de ejemplo sin cambiar la forma de uso. Si encuentro una respuesta positiva de su parte, escribiré sobre la nueva implementación.





Gracias a todos por su atención.








All Articles