RORK LABJP
TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the roundTOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Articles/Dev Tools
Dev Tools/2026-04-23Intermediate

Eliminating Flicker and Scroll Conflicts in Pull-to-Refresh for Rork Apps

If pull-to-refresh in your Rork app flashes white, jumps to the top, or leaves the spinner hanging, the fix is almost always in how refreshing state and scroll view nesting are handled. Here are the patterns that make it feel solid.

Rork515React Native209Pull to RefreshRefreshControlUX5

You add pull-to-refresh to your Rork app, drag down, let go—and the list flashes white for a split second. Or worse, the scroll position snaps to the top even though the user was halfway down the feed. Once you notice it, you can't un-notice it.

RefreshControl itself is a tiny API. Wiring it up takes less than ten minutes. The reason things still feel janky is almost always one of two problems: how the refreshing state is synchronized with the actual fetch, or how scroll views are nested. Below is a walkthrough of the exact traps I've hit in Rork-generated apps, with before-and-after code for each.

The goal is a refresh that shows a smooth indicator, keeps scroll position, and always stops loading when it's supposed to.

How RefreshControl and ScrollView fit together

Behind the scenes, Rork generates React Native components, so the plumbing is the same as any React Native app. You attach a RefreshControl to a ScrollView or FlatList via the refreshControl prop.

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

Nothing wrong with this on its own. What breaks is the coupling between the refreshing boolean and your data-fetching logic. If refreshing stays true, the spinner never stops. If you flip it to false too early, users don't feel like anything actually happened. That out-of-sync moment is where roughly 80% of flicker and scroll-jump bugs live.

The classic flickering implementation

Here's the pattern I wrote the first time I built this in Rork, which came straight out of the generated code with only minor tweaks. It looks reasonable. It is not.

// ❌ This flickers
function FeedScreen() {
  const [items, setItems] = useState([]);
  const [refreshing, setRefreshing] = useState(false);
 
  useEffect(() => {
    fetchItems().then(setItems);
  }, []);
 
  const handleRefresh = async () => {
    setRefreshing(true);
    setItems([]); // ← the culprit
    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} />
      }
    />
  );
}

The offender is setItems([]). The list is emptied before the new data arrives, so the old rows disappear for a frame and you see a white flash. Because the data array is temporarily empty, FlatList resets its scroll position. Users who were reading halfway down the feed are yanked back to the top.

Official docs don't tell you to clear old data first. The reason Rork sometimes generates this shape is pattern matching—"refresh means reset, right?" It's the single most common anti-pattern I see in generated code.

Fix 1: Just replace the data

The simplest fix that works: drop the setItems([]) line. Let React reconcile the old and new arrays.

// ✅ No flicker
const handleRefresh = async () => {
  setRefreshing(true);
  try {
    const fresh = await fetchItems();
    setItems(fresh); // overwrite directly
  } catch (err) {
    console.warn("refresh failed:", err);
  } finally {
    setRefreshing(false);
  }
};

React diffs the two arrays by key. Rows with the same id keep their native views, which means scroll position is preserved and there's no repaint of unchanged content.

The try / finally wrapper matters more than it looks. Without it, a failed fetch leaves refreshing stuck on true, and the spinner never stops. This will absolutely happen in production, so make it non-optional.

Fix 2: Keep keyExtractor stable

The replace-don't-clear pattern only works when React can identify each row. The keyExtractor is what makes that possible.

// ❌ Unstable (index breaks on reorder)
keyExtractor={(_, index) => String(index)}
 
// ✅ Stable (server-side id)
keyExtractor={(item) => item.id}

Using index means any reordering makes every row look like a new one, forcing a full redraw. That's another flicker source. Rork occasionally generates index-based keys, especially on early prompts—catch it once the list works and correct it with a follow-up prompt.

If rows have a fixed height, adding getItemLayout makes scrolling noticeably smoother because the list no longer measures each row as it appears.

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} />
  }
/>

For related symptoms—FlatLists rendering blank, scroll stuttering—I've written a separate troubleshooting flow: FlatList rendering blank in Rork apps—how to narrow down the cause. Worth having open when debugging.

Fix 3: Avoid nested scrolls

The other trap is nesting a FlatList inside a ScrollView. Ask Rork for "a feed below a hero image" and you might get exactly that layout. Put pull-to-refresh on the outer ScrollView and it fights with the inner FlatList—on iOS both move slightly, on Android it sometimes stops responding altogether.

The rule is: pull-to-refresh belongs on exactly one scrollable component. If you want a header above the list, use the FlatList's built-in ListHeaderComponent instead of wrapping everything in a ScrollView.

// ✅ No nesting
<FlatList
  data={items}
  keyExtractor={(item) => item.id}
  renderItem={({ item }) => <Row item={item} />}
  ListHeaderComponent={<HeroBanner />}
  ListEmptyComponent={<EmptyState />}
  refreshControl={
    <RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
  }
/>

ListHeaderComponent scrolls with the list. If you need a header that stays pinned, put it outside the FlatList and give the FlatList flex: 1 so it fills the remaining space cleanly.

Fix 4: Style the indicator to match your app

iOS and Android render different defaults. iOS has a spinner that scales as you pull; Android drops a circular indicator from the top. The default white spinner is invisible on a light-themed header, and hard to read on a colorful gradient background.

<RefreshControl
  refreshing={refreshing}
  onRefresh={handleRefresh}
  tintColor="#ff6b35"                // iOS spinner color
  colors={["#ff6b35", "#004e89"]}    // Android colors (cycles through)
  progressBackgroundColor="#ffffff"  // Android background
  progressViewOffset={8}             // push indicator down (px)
/>

I usually push progressViewOffset down by 8–16 pixels. It keeps the indicator visible even when there's a partially transparent header overlay. These small touches are what make an app feel designed rather than generated.

Surfacing errors gracefully

Refresh is not a guaranteed operation. Weak signal areas will fail the fetch. Catching it is step one; letting the user know is step two.

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("Couldn't refresh", "Please try again when your connection is stronger.");
  } finally {
    setRefreshing(false);
  }
};

Haptic feedback communicates success or failure without relying on sight. For more on when each pattern feels right, I've collected the conventions in The Rork haptic feedback complete guide.

A pre-push checklist

Before you commit the change, run through this list.

  • No setItems([])-style reset. Replace old data with new; don't clear first.
  • keyExtractor uses a stable server-side id (not index).
  • try / finally guarantees setRefreshing(false) no matter what happens.
  • No ScrollView wrapping a FlatList. Use ListHeaderComponent instead.
  • tintColor or colors match your app's theme, not the platform default.
  • Refresh failures surface as a toast, alert, or haptic—some sign the user can read.

The first two items kill the flicker and the scroll-jump. The rest are what take it from "works" to "feels good."

Where to go next

Pick your most-used list screen and apply this pattern first. One screen is enough to internalize the shape; after that, rolling it out to the rest of the app is mechanical. When you prompt Rork for new list screens, add a line like "on refresh, replace the data in-place instead of clearing—and use item.id for keyExtractor." That alone makes the generated code behave.

If you want to push list performance further, the Rork app performance optimization complete guide walks through memoization, image lazy-loading, and render avoidance. It's the natural next step after your refresh stops flickering.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Dev Tools2026-04-21
Building a Settings Screen That Actually Works in Your Rork App — Notifications, Subscription Management, and Legal Requirements
A practical guide to building production-grade settings screens for your Rork app, covering notifications, subscription management, legal disclosures, and support flows — with real code examples.
Dev Tools2026-07-15
The Comma That Fell to the Start of a Line: Rork, Quote Cards, and Japanese Line Breaking
React Native's Text component does not guarantee Japanese line-breaking rules. Here are the violation rates I measured, a WORD JOINER implementation that survives copy and VoiceOver, an onTextLayout audit harness, and how Rork Max (SwiftUI) differs.
Dev Tools2026-07-15
My Rork sleep timer faded out on time — but the sound didn't
Rork writes sleep-timer fades against the wall clock with setInterval. The numbers say 30 minutes, but the real audio fades early or cuts out abruptly. Here is how to drive the fade from the actual playback position, why a logarithmic curve sounds natural, and the honest limit of JS fades when the screen is locked.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →