🙆

React の props や state の変更を監視してくれる簡易的なデバッグ hook

2 min read 2

つい最近のこと。

あるコンポーネントが無駄にレンダリング回数が多くて、それのデバッグをする必要がありました。

props や state が変わればレンダリングされるのは当たり前なことなのですが、何がどう変わってレンダリングされたか、は追うのが結構難しいです。

なぜかというと、 props や state が多ければ多いほど、手動でどの値が変わってるのかを確認する必要があるからです。

そこで紹介したいのが、この useDebug hook です。

とりあえず全体像はこんな感じです。

// 実際に使う hook
export const useChangeDebugger = (props) => {
  const previousValue = usePrevious(props);

  const getChange = getChanges(previousValue, props);

  if (getChange) {
    getChange.forEach((change) => console.log(change));
  }
};

const usePrevious = (props) => {
  const previousValue = React.useRef(null);

  React.useEffect(() => {
    previousValue.current = props;
  });

  return previousValue.current;
};

function getChanges(previousValue, currentValue) {
  if (
    typeof previousValue === "object" &&
    previousValue !== null &&
    typeof currentValue === "object" &&
    currentValue !== null
  ) {
    return Object.entries(currentValue).reduce((acc, cur) => {
      const [key, value] = cur;
      const oldValue = previousValue[key];

      if (value !== oldValue) {
        acc.push({
          name: key,
          previousValue: oldValue,
          currentValue: value,
        });
      }

      return acc;
    }, []);
  }

  if (previousValue !== currentValue) {
    return [{ previousValue, currentValue }];
  }

  return [];
}

何をしているかというと、前の値(usePrevious)と現在の値を比べて、もし違ったらオブジェクトでどの値が変わったか console にログする、といった至ってシンプルな hook です。

使い道は、こういう風に

const Counter = (props) => {
  useChangeDebugger(props);

  return <span>{props.count}</span>;
};

すると、レンダリングされるたびにコンソールに表示されます:

{ key: 'count', previous: 0, current: 1 }

これだけでもデバッグ hook としては便利で成り立つのですが、こういうこともできちゃいます:

const useEffectDebugger = (fn, deps) => {
  useChangeDebugger(deps);

  return React.useEffect(fn, deps);
};

const useMemoDebugger = (fn, deps) => {
  useChangeDebugger(deps);

  return React.useMemo(fn, deps);
};

const useCallbackDebugger = (fn, deps) => {
  useChangeDebugger(deps);

  return React.useCallback(fn, deps);
};

React の元からある function をラップして、デバッグ機能を加えました。

使い方は簡単で、元の use 関数を ↑ に変えるだけ。

こんな風に

const Counter = (props) => {
  useEffectDebugger(() => {
    document.title = `clicked ${props.count} times`;
  }, [props.count]);
};

すると、こんな感じでログが出ます

{ name: "0", previousValue: 0, currentValue: 1 }

name: "0" と出てる理由は、deps は配列なので、Object.entries(currentValue) とすると index が key として取得されるからです。 index を照らし合わせて、どこが変わったか見極めることができます。

これで、 useEffect などで無限ループを起こしている値を簡単に絞り込むことができます。

ただ不足な点としては、 eslint/rule-of-hooks が適用されません。

なので、 eslint を満足させてから切り替えて debug する、と言ったフローになると思います。

debug 終えたら元の関数に戻すことを忘れずに!

Discussion

はじめまして。
文中に現れる useChangeDebugger は useDebug に読み替えれば良いでしょうか?

初めまして!
そうです、すみません。訂正します!ありがとうございます。

ログインするとコメントできます