useCallback は、関数の再生成を防ぐために導入されるReactのフックですが、その依存配列が正しく設定されていない場合、意図した挙動にならなかったり、バグの原因になったりすることがあります。

特に内部で参照する値と依存配列に指定している値の整合が取れていないケースは、レビューで最も見落とされやすい問題のひとつです。本稿では、useCallback の依存配列に関する典型的な落とし穴とその構造的判断ポイントを、レビューアー視点で詳細に掘り下げます。

よくあるミス:依存変数を含めていない

const handleSubmit = useCallback(() => {
  sendData(formValues);
}, []);
@Reviewer
`formValues` を参照しているにもかかわらず、依存配列が空配列になっています。変数の参照と依存指定が一致しておらず、意図しない古い値が使われる可能性があります。

このようなコードでは、開発者が「関数は1回だけ生成されればよい」と考えて [] にしてしまっている可能性があります。しかし、formValues は再生成され得るstateであるため、依存として明示しなければ、再生成されるべきタイミングで関数が更新されない問題が発生します。

バグが起きる仕組み

依存配列に誤りがあると次のような構造的バグが発生します。

  • 古いstateを持った関数が再利用されてしまう(データの更新漏れ)
  • 子コンポーネントに渡しても、必要なときに再レンダリングされない
  • useEffectの中で依存している関数が正しく更新されず、意図しない動作が起きる

このような問題は、再現性が低く、気付きにくいバグとして後から現れるため、レビュー段階で構造的に指摘しておくことが重要です。

ESLintが検出しても安心できない理由

React公式のESLintプラグインは、依存配列の不足を検出してくれます。ただし、それはあくまで構文的なものであり、設計意図と依存整合性のチェックはできません。

たとえば以下のようなケースでは、ESLintの自動fixによって formValues が依存に追加されます。

const handleSubmit = useCallback(() => {
  sendData(formValues);
}, [formValues]);

しかし、開発者の意図としては「submitは最初のform値だけを送る」と思っていたとすると、fix後の構造が意図とズレてしまうことになります。

%% @Reviewer %% ESLintで依存が自動補完されていても、関数の設計意図と依存の一致はレビューで明示的に確認する必要があります。

外部変数依存の構造を見抜くには

以下のような記述を見たら、依存に含めるべき外部変数があるかを読み解く必要があります。

const handleClick = useCallback(() => {
  logger.debug("Clicked", user.id);
  setLoading(true);
}, []);

ここでは user.id に依存していますが、user 自体がstateやcontextから来ている場合、その値が変化しても handleClick は再生成されません。

const user = useContext(UserContext);
const handleClick = useCallback(() => {
  sendLog(user.id);
}, []);
@Reviewer
`user` は `useContext` で得られる値であり、参照が変わる可能性があります。依存配列に含めることで再生成の正当性が担保されます。

依存として書かれていても意味をなさないケース

以下のように .current を依存配列に入れるケースも要注意です。

const ref = useRef();
const handle = useCallback(() => {
  console.log(ref.current?.value);
}, [ref.current]);
@Reviewer
`ref.current` はプリミティブではなく、評価タイミングによっては依存の役割を果たしません。参照が変わらない限りReactはこの変更を検知しないため、実質的に意味のない依存です。

依存に含めることで安心してしまうのではなく、Reactが再評価のトリガーとして機能する対象かどうかを判断する必要があります。

依存を避ける構造的リファクタの提案

必要な変数を依存に含めるのが難しい場合(たとえば関数定義時に変化させたくないとき)は、関数外に処理を切り出すことで解決する場合があります。

function createSubmitHandler(values: FormValues) {
  return () => send(values);
}

const handleSubmit = useMemo(() => createSubmitHandler(formValues), [formValues]);

この構造では formValues を依存として明示しつつ、関数定義の責務を外に出すことで useCallback の中が簡素化され、依存の明示性と構造の明瞭性が両立されます。

依存関係レビューの指摘テンプレート集

const execute = useCallback(() => {
  submit(data);
}, []);
@Reviewer
`data` に依存しているにもかかわらず、依存配列が空になっています。関数内で参照する変数は、再生成のトリガーとして明示すべきです。
const handler = useCallback(() => {
  toggle(flag);
}, [flag]);
@Reviewer
`flag` を依存に含めてはいますが、この構造は `toggle` 自体が関数の中で定義されていないため、さらに `toggle` の依存性も評価すべきです。

依存の整理と構造判断は責務設計そのもの

依存の設定は、「なにが変化のトリガーになっているか」を設計する行為に等しく、これはロジックとUIの同期構造の設計そのものです。レビューでは、単にESLintが通っているかだけでなく、その関数が本当に再生成されるべきかどうかをロジックの意味から判断してください。

レビューアーの観点チェックリスト

useCallbackの依存配列に関するレビュー観点
  • 関数内で参照している変数と依存配列が一致しているか
  • 依存配列が空配列の場合、その設計意図は明示されているか
  • 参照している変数が再生成される可能性があるものか(state, contextなど)
  • 依存に入れても意味をなさない変数(ref.currentなど)が含まれていないか
  • 自動補完された依存が設計意図に反していないか

useCallbackは、最適化の手段であると同時に、ロジックと依存の整合を設計する責任の現れです。レビューではその整合性を確実に見抜き、開発者が「意図を明示できる構造」に導けるよう判断を加えることが求められます。