Rork でアプリを生成して動かしてみると、コンソールに警告が大量に流れる場面に出くわすことがあります。よく見ると同じログが際限なく表示されていて、アプリが固まってしまっている——これが useEffect の無限ループです。
AI が生成したコードは構造的には正しく見えるのに、依存配列の微妙なミスや、オブジェクトの参照比較の落とし穴が原因でループが起きます。私自身、Rork で生成したコードで何度かこの問題にぶつかりました。原因のパターンは限られているので、チェックリストとして覚えておくと素早く解決できます。
useEffect 無限ループとはどんな現象か
React Native / Expo では、コンポーネントがレンダリングされるたびに useEffect 内の処理が走ります。通常は第2引数の依存配列(dependency array)で「何が変わったときに再実行するか」をコントロールします。
無限ループになるのは、useEffect の中でステート(state)を更新する処理があり、そのステートが依存配列に含まれている場合です。処理の流れはこうなります。
- コンポーネントがマウントされる
useEffectが実行されるuseEffect内でsetStateが呼ばれる- ステートが変わって再レンダリングが起きる
- 依存配列の値が変わったと判断され、また
useEffectが実行される - → 2 に戻る(無限ループ)
Rork が生成するコードでは特にデータ取得(fetch)や初期化処理でこのパターンが起きやすく、気づかないうちにアプリのパフォーマンスを大きく落とします。
よくある原因パターン 5 つ
パターン 1:依存配列に更新先のステートを入れてしまう
最もよくあるミスです。
// ❌ これがダメなパターン
const [data, setData] = useState([]);
useEffect(() => {
fetchData().then((result) => {
setData(result); // data を更新
});
}, [data]); // でも data を依存配列に入れているfetchData が完了して setData(result) が呼ばれると data が変わります。すると依存配列の data も変わったと判定されて useEffect がまた実行されます。これが無限ループの完成です。
// ✅ 正しい書き方:初回マウント時だけ実行したいなら空配列
const [data, setData] = useState([]);
useEffect(() => {
fetchData().then((result) => {
setData(result);
});
}, []); // 空配列 = マウント時の1回だけ実行「特定の値が変わったときだけ再取得したい」場合は、トリガーとなる値だけを依存配列に入れます。更新先のステート自体を入れないのがポイントです。
パターン 2:オブジェクトや配列を依存配列に入れている
React の依存配列の比較は参照比較(===)です。オブジェクトや配列は毎回のレンダリングで新しい参照が作られるため、中身が同じでも「変わった」と判断されます。
// ❌ 再レンダリングのたびに新しいオブジェクトが作られてループ
const config = { userId: "123", limit: 20 }; // レンダリングのたびに新規作成
useEffect(() => {
fetchUserData(config);
}, [config]); // 毎回新しい参照なのでループ// ✅ プリミティブ値に分解して依存配列に入れる
const userId = "123";
const limit = 20;
useEffect(() => {
fetchUserData({ userId, limit });
}, [userId, limit]); // プリミティブ値は値比較になるRork が生成するコードでは、const options = { headers: {...} } のような書き方がコンポーネント内に直接書かれているケースが多く、このパターンに引っかかりやすいです。
パターン 3:コールバック関数を依存配列に入れている
関数も参照比較です。コンポーネント内で定義した関数を依存配列に入れると、毎回新しい関数として判定されます。
// ❌ loadData 関数がレンダリングのたびに再生成されてループ
const loadData = () => {
fetchData().then(setData);
};
useEffect(() => {
loadData();
}, [loadData]); // 毎回新しい関数参照// ✅ useCallback でメモ化する
import { useCallback } from "react";
const loadData = useCallback(() => {
fetchData().then(setData);
}, []); // loadData の依存配列を定義
useEffect(() => {
loadData();
}, [loadData]); // メモ化されているので参照が安定するまたは、関数をコンポーネントの外に出してしまうのが最もシンプルな解決策です。
// ✅ コンポーネント外に定義するとより安全
const fetchAndSet = (setData) => {
fetchData().then(setData);
};
// コンポーネント内
const MyComponent = () => {
const [data, setData] = useState([]);
useEffect(() => {
fetchAndSet(setData);
}, []); // setData は setState 関数なので参照が安定している
return <View>{/* ... */}</View>;
};パターン 4:依存配列の警告を無視して全部追加している
ESLint の exhaustive-deps ルールが「この値を依存配列に追加してください」と警告したとき、とりあえず追加したら逆にループになった——という経験をした方も多いのではないでしょうか。
警告への対処は「全部追加する」ではなく、「なぜその値が依存配列に必要なのかを考える」ことです。追加すべき値かどうかは、次の質問で判断できます。
「その値が変わったとき、useEffect を再実行させたいか?」
- Yes → 依存配列に入れる(ただし、その処理がステートを更新してまたループしないかチェック)
- No → 依存配列に入れない、または
useRefで管理する
useRef を使うと、依存配列に影響せずに最新の値を参照できます。
// ✅ useRef で最新値を保持しつつ依存配列を安定させる
const [userId, setUserId] = useState("initial");
const userIdRef = useRef(userId);
useEffect(() => {
userIdRef.current = userId;
}); // 依存配列なし = 毎回同期(意図的)
useEffect(() => {
// userIdRef.current で最新の userId を参照
fetchUserData(userIdRef.current).then(setData);
}, []); // userId の変化に関わらず初回のみ実行パターン 5:親から渡された不安定な関数を依存配列に入れている
Rork が生成するコードでは、onUpdate や onChange のような props として渡された関数を useEffect 内で呼ぶパターンがよくあります。
// ❌ 親が毎回新しい関数を渡してくるとループになる
const ChildComponent = ({ onDataLoaded }) => {
useEffect(() => {
fetchData().then((data) => {
onDataLoaded(data); // 親から来た関数
});
}, [onDataLoaded]); // 親が毎回新関数を作るとループ
};
// 親コンポーネントの問題箇所
const Parent = () => {
// アロー関数はレンダリングのたびに新規作成される
return <ChildComponent onDataLoaded={(data) => console.log(data)} />;
};// ✅ 親側で useCallback を使う
import { useCallback } from "react";
const Parent = () => {
const handleDataLoaded = useCallback((data) => {
console.log(data);
}, []);
return <ChildComponent onDataLoaded={handleDataLoaded} />;
};または子コンポーネント側で useRef を使って最新の関数を保持する方法もあります。
// ✅ useRef で関数の最新値を保持(子コンポーネントで解決する場合)
const ChildComponent = ({ onDataLoaded }) => {
const callbackRef = useRef(onDataLoaded);
useEffect(() => {
callbackRef.current = onDataLoaded;
}); // 依存配列なし = 毎回最新に同期
useEffect(() => {
fetchData().then((data) => {
callbackRef.current(data); // ref経由で呼ぶことでループを防ぐ
});
}, []); // データ取得は初回のみ
};無限ループを見つけるデバッグ手順
「どの useEffect がループしているか分からない」という状況も多いと思います。次の手順で絞り込めます。
ステップ 1:コンソールログで確認
まずシンプルにログを入れます。
useEffect(() => {
console.log("useEffect 実行:", new Date().toISOString());
// ... 処理
}, [依存配列]);同じ時刻が連続して大量に出ていれば、そのエフェクトがループしています。
ステップ 2:依存配列の変化を確認
どの依存値が変化しているかを特定します。
const prevDepsRef = useRef({});
useEffect(() => {
const changed = {};
if (userId \!== prevDepsRef.current.userId) changed.userId = [prevDepsRef.current.userId, userId];
if (data \!== prevDepsRef.current.data) changed.data = "changed";
if (Object.keys(changed).length > 0) {
console.log("依存値の変化:", changed);
}
prevDepsRef.current = { userId, data };
}, [userId, data]);ステップ 3:コメントアウトで原因を絞り込む
useEffect 内の処理を一つずつコメントアウトして、どの setState がループを引き起こしているか特定します。
useEffect(() => {
fetchData().then((result) => {
// setData(result); // ← コメントアウトしてループが止まればここが原因
setLoading(false);
// setError(null); // ← これも同様に確認
});
}, [data]);Rork の AI に修正を依頼するときのプロンプト
Rork の AI にこの問題を修正させるときは、具体的に状況を伝えると精度が上がります。
useEffect が無限ループしています。useEffect 内で setData を
呼んでいますが、data も依存配列に入れてしまっているため
繰り返し実行されています。
初回マウント時だけデータを取得し、その後はループしないように
修正してください。eslint-disable コメントで警告を消す方法は
避けて、依存配列を正しく設定する形で直してください。
問題のコンポーネント:
[ここにコードを貼り付ける]
「eslint-disable で誤魔化さないでください」と明示するのがポイントです。AI はたまにこのコメントで警告を消すことで「解決」しようとすることがあります。
よくある間違い:空配列を付ければすべて解決はしない
「依存配列を空にすれば絶対ループしない」と考えて、全ての useEffect に [] を付けてしまうケースがあります。これは別の問題を起こします。
// ❌ userId が変わってもデータを再取得しない
const [userId, setUserId] = useState("user1");
const [data, setData] = useState(null);
useEffect(() => {
fetchUserData(userId).then(setData);
}, []); // userId が変わっても再実行されない// ✅ userId が変わったときだけ再取得する
useEffect(() => {
fetchUserData(userId).then(setData);
}, [userId]); // userId が変わったときだけ実行更新先のステートは依存配列に入れない、でもトリガーとなる値(この場合は userId)は入れる——このバランスが大切です。
まず依存配列を疑う
useEffect がループしているときのチェックポイントを整理します。
- 依存配列に更新先のステート自体が入っていないか
- 依存配列にオブジェクト・配列・関数が直接入っていないか
- 依存配列に親から渡された不安定な関数が入っていないか
これで解決しないケースは稀です。たいていは useCallback / useMemo でメモ化するか、依存配列から外してロジックを整理することで直ります。
状態管理まわりの設計については、Rork アプリの高度な状態管理パターン も参考になります。パフォーマンス全体を見直したい場合は Rork アプリのパフォーマンス最適化完全ガイド もあわせてご覧ください。