A little practice with JS Proxy to optimize redrawing of React components when using useContext

The problem we are solving

The context in react can contain many values ​​and different consumers of the context can use only part of the values. However, when any value changes from the context, all consumers (in particular, all components that use useContext



) will be rerendered , even if they do not depend on the changed part of the data. The problem is quite discussed and has many different solutions. Here are some of them. I created this example to demonstrate the problem. Just open the console and press the buttons.





purpose

Our solution should change the existing codebases to a minimum. I want to create my own custom hook useSmartContext



 with the same signature as that of useContext



, but which will only re-render the component when the used part of the context changes.





Idea

Find out what is being used by the component by wrapping the return useSmartContext



value in a Proxy.





Implementation

Step 1.





We create our own hook.





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



We have created a list in which we will store the used context fields. We created a proxy with a get



 trap in which we fill this list. Target



it doesn't matter to us, so I passed an empty object as the first argument {}



.





Step 2.





You need to get the value of the context when it is updated and compare the value of the fields from the list usedPropsRef



with the previous values. If something has changed, then trigger a re-rendering. useContext



We cannot use it inside our hook, otherwise our hook will also start causing re-rendering for all changes. Here dances with a tambourine begin. I originally hoped to subscribe to context changes with context.Consumer



. Namely like this:





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





















, . .





While writing this article, I came across another library that solves the same problem with optimizing redraws when using context. The solution of this library, in my opinion, is the most correct one I have seen. Its sources are much more readable and they gave me a couple of ideas on how to make our example production ready without changing the way it is used. If the meeting received a positive response from you, then I will write about the new implementation.





Thank you all for your attention.








All Articles