useContextを使用するときにReactコンポーネントの再描画を最適化するためのJSプロキシのちょっとした練習

私たちが解決している問題

reactのコンテキストには多くの値を含めることができ、コンテキストのさまざまなコンシューマーは値の一部のみを使用できます。ただし、コンテキストから値が変更useContext



されると、データの変更された部分に依存していなくても、すべてのコンシューマー(特に、を使用するすべてのコンポーネント)が再レンダリングされます。この問題はかなり議論されており、さまざまな解決策があります。ここにそれらのいくつかがあります。このは、問題を示すために作成しましたコンソールを開いてボタンを押すだけです。





目的

私たちのソリューションは、既存のコードベースを最小限に変更する必要があります。useSmartContext



 同じ署名を使用して独自のカスタムフックを作成したいのですuseContext



が、コンテキストの使用部分が変更された場合にのみコンポーネントを再レンダリングします。





考え

戻りuseSmartContext



値をプロキシでラップして、コンポーネントで何が使用されているかを調べます。





実装

ステップ1。





独自のフックを作成します。





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



使用されたコンテキストフィールドを格納するリストを作成しました。get



 このリストを埋めるトラップを使用してプロキシを作成しましたTarget



それは私たちには関係ないので、最初の引数として空のオブジェクトを渡しました{}







ステップ2。





更新時にコンテキストの値を取得し、リストのフィールドの値をusedPropsRef



以前の値と比較する必要があります。何かが変更された場合は、再レンダリングをトリガーします。useContext



フック内で使用することはできません。そうしないと、フックもすべての変更に対して再レンダリングを引き起こし始めます。ここでタンバリンを使ったダンスが始まります。私はもともと、コンテキストの変更をでサブスクライブしたいと思っていましたcontext.Consumer



つまり、このように:





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





















, . .





この記事を書いているときに、コンテキストを使用するときに再描画を最適化することで同じ問題を解決する別のライブラリに出くわしました私の意見では、このライブラリの解決策は私が見た中で最も正しいものです。そのソースははるかに読みやすく、使用方法を変更せずにサンプルプロダクションを準備する方法についていくつかのアイデアを与えてくれました。あなたからの肯定的な反応に出会ったら、新しい実装について書きます。





ご清聴ありがとうございました。








All Articles