React の props や state の変更を監視してくれる簡易的なデバッグ hook
つい最近のこと。
あるコンポーネントが無駄にレンダリング回数が多くて、それのデバッグをする必要がありました。
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 に読み替えれば良いでしょうか?
初めまして!
そうです、すみません。訂正します!ありがとうございます。