本番では動くのに開発環境で動かない!StrictModeとuseEffectの落とし穴
開発環境でだけ認証処理が失敗する謎のバグ。原因はReact StrictModeによるuseEffectの二重実行だった。
ちょいネタですが、地味にハマる人がいるかもしれないので共有。
開発環境でだけOAuth認証のコールバック処理が失敗するという現象に遭遇しました。ローカル以外の環境では問題なく動くのに、ローカルでだけログインに失敗する。コードは全く同じなのに。
原因はReact StrictModeでした。
何が起きていたか
コールバックを処理するコンポーネントで、useEffect内で非同期処理と画面遷移を行っていました。
function CallbackPage() {
const navigate = useNavigate();
useEffect(() => {
const handleCallback = async () => {
// 何らかの非同期処理(1回だけ実行したい)
await someAsyncProcess();
navigate("/home");
};
handleCallback();
}, [navigate]);
return <div>処理中...</div>;
}
一見問題なさそうに見えますよね。
StrictModeの挙動
React 18のStrictModeでは、開発環境において意図的にuseEffectを2回実行するという挙動があります。
これはコンポーネントが「マウント → アンマウント → 再マウント」される挙動をシミュレートすることで、副作用のクリーンアップが正しく実装されているかをチェックするためのものです。
つまり、上記のコードでは:
- 1回目の実行:
someAsyncProcess()→navigate() - 2回目の実行:
someAsyncProcess()→navigate()
という具合に、処理が2回走ってしまっていました。これが失敗や無限ループっぽい挙動の原因でした。
本番では発生しない理由
StrictModeの二重実行は開発モード(NODE_ENV=development)でのみ発生します。
本番ビルドではuseEffectは1回しか実行されないため、ローカル以外の環境では問題が発生しませんでした。
対処法
useRefを使って、処理が既に実行中かどうかを追跡します。
function CallbackPage() {
const navigate = useNavigate();
const isProcessingRef = useRef(false);
useEffect(() => {
// StrictModeで2回実行されるのを防ぐ
if (isProcessingRef.current) {
return;
}
isProcessingRef.current = true;
const handleCallback = async () => {
await someAsyncProcess();
navigate("/home");
};
handleCallback();
}, [navigate]);
return <div>処理中...</div>;
}
ポイントはuseStateではなくuseRefを使うこと。useStateだと依存配列に追加する必要があり、状態更新でさらに再レンダリングが発生してしまいます。useRefなら再レンダリングを引き起こさずに値を保持できます。
なぜuseStateではダメなのか
// これはNG
const [isProcessing, setIsProcessing] = useState(false);
useEffect(() => {
if (isProcessing) return;
setIsProcessing(true);
// ...
}, [isProcessing]); // ← isProcessingを依存配列に入れるとLintに怒られる
// 入れないとLintエラー、入れると無限ループの可能性
useRefなら依存配列に含める必要がないので、この問題を回避できます。
地味なハマりポイントですが、知っていれば一瞬で解決できる問題です。
開発環境だけ動かないバグに遭遇したら、StrictModeを疑ってみてください。useEffectで「1回だけ実行したい処理」にはuseRefでガードを入れるのがおすすめです。特に認証やAPI呼び出しなど、冪等でない処理は注意が必要なので、同じ現象に遭遇した方の参考になれば。