React における useCallback の依存関係と古いステートの罠
React では、関数を useCallback
でメモ化することで、無駄な再生成を防ぎ、パフォーマンスの最適化を行うことができます。しかし、依存配列を適切に指定しないと「古いステートを持ち続ける」という予期せぬ挙動を引き起こすことがあります。
🔁 問題の例
以下のコードを見てください:
const Component = () => {
const [count, setCount] = useState(0);
const funcA = useCallback(() => {
console.log('count in funcA:', count);
}, [count]); // ✅ count に依存している
const funcB = useCallback(() => {
funcA(); // 古い funcA が使われる可能性
}, []); // ⚠️ funcA を依存配列に含めていない
return (
<div>
<button onClick={funcB}>Click</button>
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
</div>
);
};
このコードでは以下の問題が起きます。
-
funcA
はcount
に依存しており、count
が更新されるたびに新しい関数になります。 - しかし
funcB
は依存配列が空なので、一度生成されたfuncA
をそのまま使い続けます。 - その結果、
funcB
の中で使われるfuncA
は 古いステート(count
)にバインドされたまま になります。
✅ 要点まとめ
項目 | 状況 | 結果 |
---|---|---|
funcA に count を依存登録 |
✅ 正しい | |
funcB に funcA を依存登録しない |
⚠️ 古い funcA を呼び続ける |
関数Bの中で使っている関数Aが新しくなっていても、関数Bが古いAを保持している場合、最新の状態を取得できない。
これは React のクロージャの性質と useCallback
の依存配列の指定方法に由来するものです。
🛠 対策
1. funcB
の依存配列に funcA
を追加する
const funcB = useCallback(() => {
funcA();
}, [funcA]); // ✅ funcA が更新されるたびに funcB も再生成
2. そもそも useCallback
を使わない
パフォーマンスの最適化が不要な場合、useCallback
を使わずにそのまま関数を定義しても問題ないケースも多いです。
💡 まとめ
-
useCallback
を使うときは 依存配列が命。 - 呼び出す関数を依存配列に入れ忘れると、古いバージョンの関数を使い続けることになる。
-
useCallback
による最適化は便利ですが、クロージャによる副作用に注意が必要です。