RORK LABEN
TOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しましたTOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しました
記事一覧/開発ツール
開発ツール/2026-04-23中級

Rork アプリの Pull to Refresh でチラつき・スクロール競合を解消する実装パターン

Rork で生成したアプリに Pull to Refresh を追加したら画面がチラつく、スクロール位置が飛ぶ、読み込みが終わらない—よくあるこの3つの症状を、実装パターンごとに解消していきます。

Rork515React Native209Pull to RefreshRefreshControlUX5

Rork で作ったアプリに「引っ張って更新」を付けたら、更新後に画面が一瞬白くチラつきます。あるいはスクロール位置がいつの間にか一番上に戻っていることもあります。こういう違和感、気になり始めると一気に気になります。

RefreshControl 自体はとてもシンプルな API で、動かすだけなら10分もかからないはずです。それなのにチラつきやスクロール競合が起きるのは、だいたい 「更新中の状態管理」「スクロールビューのネスト構造」 のどちらかに原因があります。今回は私が Rork 製アプリで実際に踏んだ落とし穴を、ビフォー・アフターのコード付きで整理していきます。

完成品としては「更新インジケータが滑らかに表示される」「更新後もスクロール位置が保持される」「ローディングが確実に止まる」の3点を目指します。

まず理解する — RefreshControl と ScrollView の関係

Pull to Refresh の仕組みは、Rork が裏で生成している React Native のコンポーネントに依存しています。構造としては ScrollView または FlatListrefreshControl プロパティに RefreshControl を渡すだけ、というシンプルなものです。

import { FlatList, RefreshControl } from "react-native";
 
<FlatList
  data={items}
  keyExtractor={(item) => item.id}
  renderItem={({ item }) => <Row item={item} />}
  refreshControl={
    <RefreshControl
      refreshing={refreshing}
      onRefresh={handleRefresh}
    />
  }
/>

このコード自体に問題はありません。落とし穴が出てくるのは、refreshing という boolean と、実際のデータ更新処理との同期がズレたときです。refreshingtrue のまま終わらないとインジケータが止まらず、逆に早すぎるタイミングで false に戻すと「ちゃんと更新された感」が出ません。この「同期のズレ」が、チラつきやスクロール戻りの原因の8割を占めます。

チラつく典型的な実装 — 何が起きているか

まずはよくあるダメなパターンを見ていきます。これは私が最初に書いた、Rork が生成したコードをそのまま少しだけ修正したバージョンです。

// ❌ チラつきが出る実装
function FeedScreen() {
  const [items, setItems] = useState([]);
  const [refreshing, setRefreshing] = useState(false);
 
  useEffect(() => {
    fetchItems().then(setItems);
  }, []);
 
  const handleRefresh = async () => {
    setRefreshing(true);
    setItems([]); // ← ここが犯人
    const fresh = await fetchItems();
    setItems(fresh);
    setRefreshing(false);
  };
 
  return (
    <FlatList
      data={items}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => <Row item={item} />}
      refreshControl={
        <RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
      }
    />
  );
}

問題は setItems([]) です。リストを一度空にしてから新しいデータを入れ直しているので、古いデータが一瞬消える瞬間があり、そこで画面が白くチラつきます。さらに、空配列になることで FlatList のスクロール位置がリセットされ、ユーザーが読んでいた位置から強制的に一番上に戻ってしまいます。

公式ドキュメントには「古いデータは消してから新しく取得してください」とは書かれていません。ですが Rork の AI はパターンマッチで「リフレッシュならまずクリアする」という書き方を選びがちで、これが現場では一番よく見るアンチパターンになっています。

修正パターン1 — データを差し替えるだけにする

一番シンプルで効く修正です。古いデータを消さず、新しいデータで 置き換える だけにします。

// ✅ チラつかない実装
const handleRefresh = async () => {
  setRefreshing(true);
  try {
    const fresh = await fetchItems();
    setItems(fresh); // 古いデータは直接上書き
  } catch (err) {
    console.warn("refresh failed:", err);
  } finally {
    setRefreshing(false);
  }
};

変更点は setItems([]) を削除しただけですが、これだけでチラつきは消えます。React の再レンダリングは新旧の配列を比較して差分だけを描画するので、同じ id を持つ行は DOM(正確には Native View)が使い回され、スクロール位置も保持されます。

try / finally で囲んでいるのも重要で、fetchItems() が失敗したときに refreshingtrue のまま残ってインジケータが回り続ける事故を防ぎます。これは地味ですが、本番では必ず起きる現象なので忘れないでおきたいところです。

修正パターン2 — keyExtractor を必ず安定させる

差し替えパターンが効くのは、「新旧のアイテムを React が同一と判定できる」ときだけです。判定の鍵を握るのが keyExtractor です。

// ❌ 不安定なキー(index は並び替えで壊れる)
keyExtractor={(_, index) => String(index)}
 
// ✅ 安定したキー(サーバー側のID)
keyExtractor={(item) => item.id}

index をキーに使うと、並び順が変わっただけで「全行が別物」と判定されて、結局全部再描画されます。これもチラつきの原因になります。Rork に「リストを作って」と頼むと、たまに index ベースのキーが生成されることがあるので、動くようになった段階で一度プロンプトで明示的に修正を入れるのが確実です。

もし画像付きのリストなら、getItemLayout も設定しておくとスクロールの滑らかさが一段上がります。アイテムの高さが一定なら、次のように書けます。

const ROW_HEIGHT = 88;
 
<FlatList
  data={items}
  keyExtractor={(item) => item.id}
  renderItem={({ item }) => <Row item={item} />}
  getItemLayout={(_, index) => ({
    length: ROW_HEIGHT,
    offset: ROW_HEIGHT * index,
    index,
  })}
  refreshControl={
    <RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
  }
/>

「FlatList が空白で描画されない」「スクロールがガタつく」といった関連症状については、FlatList が空白で何も映らない—Rork アプリのリスト崩れを切り分ける にチェック順序をまとめていますので、あわせて読んでいただけると切り分けが早くなります。

修正パターン3 — ネストしたスクロールでの競合を避ける

もう一つハマりやすいのが、ScrollView の中に FlatList を入れているケースです。Rork に「ヘッダー画像の下にフィードを表示して」とお願いすると、外側を ScrollView、内側を FlatList にしたコードが生成されることがあります。

このとき Pull to Refresh を外側の ScrollView に付けると、内側の FlatList のスクロールと競合して「どちらも少しずつ動く」という奇妙な挙動になります。iOS ではギリギリ動いても、Android では反応しなくなることもあります。

解決策は「Pull to Refresh を付けるのは、実際にスクロールするコンポーネント一つだけ」という原則です。外側にヘッダーを置きたい場合は、FlatListListHeaderComponent を使うのが王道です。

// ✅ ネストを避ける
<FlatList
  data={items}
  keyExtractor={(item) => item.id}
  renderItem={({ item }) => <Row item={item} />}
  ListHeaderComponent={<HeroBanner />}
  ListEmptyComponent={<EmptyState />}
  refreshControl={
    <RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
  }
/>

ListHeaderComponent はスクロールと一緒に上に流れていきます。ヘッダーを固定したいなら、FlatList の外に別コンポーネントとして置き、FlatList 自身には flex: 1 を指定して残り領域を全部使わせるのがクリーンです。

修正パターン4 — インジケータの色と位置を整える

Android と iOS ではデフォルトのインジケータ表現が違います。iOS は引っ張った量に応じてくるくる回るスピナーで、Android は少し下がったところに丸いインジケータがポップアップします。アプリのテーマが暗めなのに、デフォルトの白いスピナーだと見えにくいことがあります。

<RefreshControl
  refreshing={refreshing}
  onRefresh={handleRefresh}
  tintColor="#ff6b35"           // iOS のスピナー色
  colors={["#ff6b35", "#004e89"]} // Android のインジケータ色(複数指定で順に切り替わる)
  progressBackgroundColor="#ffffff" // Android の背景色
  progressViewOffset={8}        // インジケータを出す位置の調整(px)
/>

私は progressViewOffset を 8〜16px くらい下げるのが好みです。ヘッダーが少し被っていても見やすくなります。デザイン重視のアプリなら、このあたりを合わせるだけで「こだわって作ってある感」が出て、印象が変わります。

エラー時に「更新失敗」を伝える

リフレッシュは必ず成功するわけではありません。電波が弱いところで引っ張ると fetch が失敗することもあります。try / catch で拾っておくだけでなく、ユーザーに軽く通知する仕組みを用意すると、UX としての完成度が上がります。

import * as Haptics from "expo-haptics";
import { Alert } from "react-native";
 
const handleRefresh = async () => {
  setRefreshing(true);
  try {
    const fresh = await fetchItems();
    setItems(fresh);
    Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
  } catch (err) {
    Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
    Alert.alert("更新に失敗しました", "電波の良い場所で再度お試しください。");
  } finally {
    setRefreshing(false);
  }
};

ハプティック(振動フィードバック)を足すと、視覚に頼らない「更新できた/できなかった」の伝達ができます。触覚フィードバックの使い分けについては、Rork アプリのハプティック(触覚フィードバック)完全ガイド にまとめています。

最小構成のチェックリスト

ここまでの内容を、push する前に確認できるリストにまとめます。

  • setItems([]) のような一旦クリアする処理を入れていない(古いデータは新しいデータで上書きするだけにする)
  • keyExtractor にはサーバー側の安定したID(item.id など)を指定している
  • refreshing の切り替えを try / finally で確実に false に戻している
  • ScrollViewFlatList を入れ子にしていない(入れ子が必要なら ListHeaderComponent を使う)
  • tintColor または colors でアプリのテーマに合わせた色を指定している
  • リフレッシュ失敗時のユーザー通知を用意しているAlert か Toast ライブラリ)

このうち最初の2つが守れていれば、チラつきとスクロール戻りはほぼ起きません。残りは「使っていて気持ちいい」レベルに上げるための仕上げになります。

次にやること

まずは今お持ちのアプリの一番よく使うリスト画面に、このパターンを適用してみてください。1画面に反映すれば感覚が掴めますし、他の画面に横展開するときも同じ考え方で整理できます。Rork にプロンプトを追加するときは「リフレッシュ時に既存データをクリアせず、新しいデータで置き換える実装にしてください。keyExtractor には item.id を使ってください」と明示すると、AI が生成するコードも安定します。

リスト周りのパフォーマンスをさらに詰めたい方には、Rork アプリのパフォーマンス最適化完全ガイド でメモ化・画像遅延読み込み・レンダリング最適化までまとめていますので、次の一手としておすすめです。

シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

開発ツール2026-04-21
Rork で作ったアプリに『ちゃんとした設定画面』を実装する — 通知・購読管理・法定表示の実装ガイド
Rork で作ったアプリの設定画面を実用レベルに仕上げる実装パターンを、通知・購読プラン管理・法定表示・サポート導線の観点から、実コードとともに丁寧に解説します。
開発ツール2026-07-15
「、」が行頭に落ちた — Rork が組んだ名言カードと日本語の禁則処理
React Native の Text は日本語の行頭禁則を保証しません。実測した違反率、WORD JOINER による事前結合の実装、onTextLayout での自動監査、Rork Max(SwiftUI)との挙動差までまとめました。
開発ツール2026-07-15
Rorkの癒しアプリにスリープタイマーを付けたら、フェードアウトが実際の音とズレた話
Rorkが生成するスリープタイマーは、壁時計のsetIntervalで音量を下げます。数値上は30分でも、実際の音はもっと早く消えたり不自然に途切れたりします。フェードを再生位置で駆動し直す実装と、対数カーブ・停止処理・画面ロック時の限界までを実運用の視点でまとめました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →