useRefを依存に使いたくなる誘惑をレビューで止める
useRef
の .current
は、Reactの再レンダリングとは無関係に値の読み書きができる特殊な領域です。この特性を利用して、useEffect
の依存配列に ref.current
を含めようとするコードを見かけることがあります。
一見「安定的で変わらない値を参照する設計」として合理的に見えるこの手法ですが、Reactの依存配列評価モデルに対する理解不足によって、かえって誤動作や設計不整合を招くケースが多く、レビューでは慎重な判断が求められます。
誘惑:useRefの値を依存に含めたくなるケース
const ref = useRef(0);
useEffect(() => {
doSomething(ref.current);
}, [ref.current]);
@Reviewer`ref.current` はプリミティブな値であっても `useEffect` の依存には適しません。Reactの依存評価は「参照が変化したか」であって「内部のプロパティが変化したか」ではないため、意図通りに動作しない恐れがあります。
このように記述しても、ref.current
が更新されても useEffect
は再実行されません。これは依存配列が ref.current
の値ではなく、評価時の値のコピーになるためです。
Reactの依存配列の評価基準
Reactの依存配列は「参照が変化したか(=== がfalse)」を評価基準にしており、オブジェクトの内部プロパティが変わってもそのオブジェクト自身の参照が変わっていなければ、依存とは見なされません。
つまり:
useState
の更新は自動的に再レンダリングと副作用を引き起こすuseRef.current
の変更はReactの監視対象ではない
この仕組みを理解していないまま ref.current
を依存に含めると、「なぜこの useEffect
が発火しないのか?」という誤解を生みます。
意図的な依存ではなく“状態の観測”であるべき
もし ref.current
の変化に応じて useEffect
を再実行したいという要件がある場合、それは本来「状態」や「イベント」によって制御されるべきであり、refを依存にするのは根本的に適切ではありません。
const [scrollY, setScrollY] = useState(0);
const scrollRef = useRef();
useEffect(() => {
const handleScroll = () => {
setScrollY(scrollRef.current?.scrollTop || 0);
};
scrollRef.current?.addEventListener("scroll", handleScroll);
return () => scrollRef.current?.removeEventListener("scroll", handleScroll);
}, []);
@Reviewer`ref.current` の内部値がトリガーになる場合は、状態として切り出し `setScrollY` のように更新をトリガーにした設計が適しています。ref自体を依存にしても、Reactはその内部変化を追跡しません。
このように、refの変化を監視したいのであれば、それはstateやイベントによる構造で行うべきです。
依存配列にrefを含めることが必要な唯一のパターン
useRef
自体の参照がコンポーネント外から更新されうるような特殊な構造(例:カスタムフック内のref変更通知など)であれば、refオブジェクト自体を依存に含めることは構文的には意味があります。
useEffect(() => {
console.log("refが切り替わった");
}, [ref]);
しかしこれは ref.current
の値変化を監視しているわけではなく、refオブジェクトそのもの(identity)に依存しているだけです。内部値の追跡ではないという点を理解していなければ、誤用を助長します。
よくある誤用パターンとレビュー例
const isFirstRender = useRef(true);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
return;
}
doSomething();
}, [isFirstRender.current]);
@Reviewer`ref.current` の変化をトリガーにしたいように見えますが、Reactはこのプロパティを依存として評価しないため、副作用の再実行はされません。依存としての役割を果たせず、誤解を招きます。
このような構造では、依存ではなく「制御用のフラグ」が useRef に隠れているため、意図が伝わりづらく、レビューの際には設計の意図を明確にする必要があります。
まとめ:refを依存にしたくなった時に問うべきこと
ref.current
の値変化をトリガーにしたいのか? → それはstate化すべき- 参照自体が切り替わる設計になっているのか? → 依存に含める理由がある
ref
を使うこと自体が副作用を隠していないか? → 設計の明示が必要- 状態か、イベントか、別の設計方法が適していないか? → 再検討
refはReactの状態管理の外側にあるため、その値をトリガーにロジックを構築しようとすると、Reactのモデルに反した設計になりがちです。レビューアーとしては、「なぜそれをrefにしたのか」「なぜそれを依存にしたいのか」を常に問い直し、構造的に伝わる設計への改善を促す必要があります。