「APIから正常にデータが返ってきているのに、画面がずっとくるくる回っている」——Rorkで作ったアプリを実機テストしていると、この状況に一度は遭遇するのではないでしょうか。
エラーメッセージが出ているわけでもなく、ログを見ると値は取れています。でも画面は更新されありません。原因が掴みにくく、AIに修正を依頼しても同じ問題が繰り返される……というのが、このタイプのバグの厄介なところです。
ここではRorkが生成するReact Nativeコードでローディングが終わらない・UIが更新されない問題を、パターン別に整理して解説します。コードの読み方がわからなくても対処できるよう、修正のポイントをできる限り具体的に説明します。
なぜこの問題が起きるのか
Rorkが生成するコードは、データ取得の処理を非同期(async/await)で書きます。処理の流れはざっくりこうなります。
- 画面が表示される
isLoading = trueにしてスピナーを表示- APIなどからデータを取得する
isLoading = falseにしてスピナーを消し、データを表示
問題は、3のステップで何かが起きたとき、4のステップが実行されないケースがあることです。「データ取得に失敗したとき」「エラーが発生したとき」「取得結果が空だったとき」——こうした例外的な状況で isLoading が false に戻らないと、スピナーが永遠に回り続けます。
パターン1: エラー発生時にローディングが解除されない
最も多い原因です。try/catch を使っているのに、finally を書いていないパターンです。
// エラーが起きるとこのパターンはローディングが解除されない
const fetchData = async () => {
setIsLoading(true);
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
setItems(data);
setIsLoading(false); // エラーが起きるとここに到達しない
} catch (error) {
console.error(error);
// isLoading を false にする処理が抜けている
}
};エラーが発生すると catch ブロックに処理が飛び、setIsLoading(false) が実行されないまま終わります。修正は finally を使います。
// finally で必ずローディングを解除するパターン
const fetchData = async () => {
setIsLoading(true);
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
setItems(data);
} catch (error) {
console.error('データ取得エラー:', error);
setError('データの取得に失敗しました');
} finally {
setIsLoading(false); // 成功・失敗を問わず必ず実行される
}
};finally ブロックに書いた処理は、try が成功しても catch に入っても、必ず実行されます。ローディング解除の処理はここに書くのが原則です。
Rorkにこの問題を修正してもらうときは「fetchData関数のtry/catchにfinallyを追加して、finallyの中でsetIsLoading(false)を呼ぶように修正してください」と具体的に指示すると確実です。
パターン2: 条件分岐でローディング解除が抜ける
データの取得後に条件によって処理を分岐しているとき、一部のパスでローディング解除が漏れるケースがあります。
// 空配列のときにローディングが残るパターン
const fetchUserData = async (userId: string) => {
setIsLoading(true);
const data = await getUserData(userId);
if (data && data.length > 0) {
setUserList(data);
setIsLoading(false); // データがある場合のみ実行される
}
// data が空配列のとき、setIsLoading(false) が呼ばれない
};この場合も finally が解決策になりますが、よりシンプルな書き方として、ローディング解除を条件分岐の外に出す方法もあります。
// ローディング解除を条件外に移動した修正版
const fetchUserData = async (userId: string) => {
setIsLoading(true);
try {
const data = await getUserData(userId);
if (data && data.length > 0) {
setUserList(data);
} else {
setEmptyMessage('ユーザーが見つかりませんでした');
}
} catch (error) {
setError('データ取得に失敗しました');
} finally {
setIsLoading(false); // どの分岐でも確実に実行
}
};パターン3: useEffect の依存配列が原因でデータ再取得がループする
逆に「ローディングが終わったと思ったらすぐまた始まる」という症状の場合、useEffect の依存配列に問題があることがほとんどです。
// 無限ループになるパターン
useEffect(() => {
fetchData();
setLastFetched(new Date()); // これが再レンダリングを引き起こす
}, [lastFetched]); // lastFetched の変更で useEffect が再実行されてしまうuseEffect の依存配列に含まれている値が、useEffect の中で変更されると無限ループになります。
// 初回マウント時のみ実行する場合(空の依存配列)
useEffect(() => {
fetchData();
}, []);
// 特定の値が変わったときだけ実行する場合
useEffect(() => {
if (userId) {
fetchData(userId);
}
}, [userId]); // userId が変わったときだけ実行(userId 自体は fetchData の中で変更しない)依存配列の中に「useEffect の中で更新している state」が含まれていないか確認してください。これがループの根本原因になっているケースが非常に多いです。
パターン4: レスポンスのデータ構造が想定と違う
APIはエラーなく200を返しているのに、画面にデータが表示されないケースです。取得したデータの構造が、コードが想定しているものと異なるときに起きます。
// APIレスポンスの実際の形: { success: true, result: { items: [...] } }
// 間違ったアクセス方法(undefined になる)
const data = await response.json();
setItems(data.items); // 本当は data.result.items
// 正しいアクセス方法
setItems(data.result.items);この問題は console.log で確認するのが一番確実です。
const fetchData = async () => {
setIsLoading(true);
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
// 実際のデータ構造をログで確認する
console.log('APIレスポンス:', JSON.stringify(data, null, 2));
// 確認した構造に合わせてアクセスする
setItems(data.result.items);
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
};Rork Companionアプリのコンソールログ、またはExpo開発サーバーのターミナルで console.log の出力を確認できます。レスポンスの中身を確認してからRorkに「data.result.items の配列をsetItemsに渡すように修正して」と伝えると正確に直してもらえます。
パターン5: コンポーネントのアンマウント後に setState が呼ばれる
画面を素早く切り替えたとき、「Can't perform a React state update on an unmounted component」という警告が出ることがあります。これは、コンポーネントが既に画面から消えた後に、非同期処理の結果がstateに反映されようとするときに起きます。
最近のReact/Expo環境では警告のみで動作への影響は限定的ですが、メモリリークの原因になるため対処しておく価値があります。
// クリーンアップ関数でアンマウント後の setState を防ぐ
useEffect(() => {
let isMounted = true; // マウント状態を管理するフラグ
const fetchData = async () => {
setIsLoading(true);
try {
const data = await getData();
if (isMounted) { // マウントされている場合のみ state を更新
setItems(data);
}
} finally {
if (isMounted) {
setIsLoading(false);
}
}
};
fetchData();
return () => {
isMounted = false; // クリーンアップ時にフラグをオフにする
};
}, []);このパターンはやや複雑なので、Rorkに「useEffectの中でisMountedフラグを使ってアンマウント後のsetStateを防ぐように修正して」と伝えると対応してもらえます。
Rorkへの修正依頼の伝え方
ここまでのパターンを踏まえて、Rorkへの修正依頼で効果的なフレーズをまとめておきます。
エラー時のローディング解除漏れには「fetchData関数のtry/catchブロックにfinallyを追加して、setIsLoading(false)をfinallyの中に移動してください」と伝えます。
無限ループには「useEffectの依存配列を確認して、配列の中で変化している値を依存配列から外してください。初回のみ実行する場合は空配列にしてください」が有効です。
データが表示されない場合は「fetchData関数の中にconsole.log(JSON.stringify(data, null, 2))を追加して、APIレスポンスの実際の構造を確認できるようにしてください」と依頼します。
トラブルシューティングの順番
ローディングが終わらない・UIが更新されない問題に直面したら、この順番で確認してみてください。
まず setIsLoading(false) の呼び出し漏れを確認します。try の中だけに書いていないか、finally に移動することで解決しないかを試します。
次に console.log でAPIレスポンスの実際の中身を確認します。想定と異なるデータ構造が原因のケースは思いのほか多いです。
その後、useEffect の依存配列を確認します。配列の中に、その useEffect の中で変化する値が入っていないかチェックします。
それでも解決しない場合は、Rorkに「fetchData関数の全体を表示して」と依頼して、データフローを一から確認するのが近道です。
非同期処理のバグはAI生成コードでも人が書いたコードでも頻繁に起きます。「エラーではないのに動かない」という状況のほとんどは、ここで紹介したいずれかのパターンに当てはまります。ひとつひとつ確認していけば、必ず原因が見つかります。