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

Yuya, web developmentreactdevtools
Back

つい最近のこと。

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

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

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

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

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

// 実際に使う hook
export const useDebug = (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) => {
useDebug(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 終えたら元の関数に戻すことを忘れずに!

© Yuya Oiwa