Um pouco de prática com JS Proxy para otimizar o redesenho de componentes React ao usar useContext

O problema que estamos resolvendo

O contexto em react pode conter muitos valores e diferentes consumidores do contexto podem usar apenas parte dos valores. No entanto, quando qualquer valor muda do contexto, todos os consumidores (em particular, todos os componentes que usam useContext



) serão renderizados novamente , mesmo se eles não dependerem da parte alterada dos dados. O problema é bastante discutido e tem muitas soluções diferentes. Aqui estão alguns deles. Criei este exemplo para demonstrar o problema. Basta abrir o console e pressionar os botões.





propósito

Nossa solução deve alterar as bases de código existentes ao mínimo. Quero criar meu próprio gancho personalizado useSmartContext



 com a mesma assinatura de useContext



, mas que apenas renderizará novamente o componente quando a parte usada do contexto for alterada.





Ideia

Descubra o que está sendo usado pelo componente envolvendo o useSmartContext



valor de retorno em um Proxy.





Implementação

Passo 1.





Criamos nosso próprio gancho.





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



Criamos uma lista na qual armazenaremos os campos de contexto usados. Criamos um proxy com uma get



 armadilha na qual preenchemos essa lista. Target



não importa para nós, então passei um objeto vazio como o primeiro argumento {}



.





Passo 2.





Você precisa obter o valor do contexto quando ele for atualizado e comparar o valor dos campos da lista usedPropsRef



com os valores anteriores. Se algo mudou, acione uma nova renderização. useContext



Não podemos usá-lo dentro de nosso gancho, caso contrário, nosso gancho também começará a causar uma nova renderização para todas as alterações. Aqui começam as danças com um pandeiro. Originalmente, esperava subscrever as mudanças de contexto com context.Consumer



. Nomeadamente assim:





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





















, . .





Ao escrever este artigo, me deparei com outra biblioteca que resolve o mesmo problema com a otimização de redesenho ao usar o contexto. A solução desta biblioteca, em minha opinião, é a mais correta que já vi. Suas fontes são muito mais legíveis e eles me deram algumas idéias sobre como preparar nossa produção de exemplo sem mudar a forma de uso. Se eu receber uma resposta positiva sua, escreverei sobre a nova implementação.





Obrigado a todos pela atenção.








All Articles