RORK LABJP
MAX — Rork Max generates native Swift for every Apple platform, from iPhone to Vision ProNATIVE — It reaches native capabilities like AR/LiDAR, Metal 3D, Dynamic Island, Live Activities, and HealthKitPUBLISH — Publish to the App Store in two clicks; Rork Max is $200/monthEXPO — Standard Rork builds iOS and Android together via React Native (Expo) and is free to startPROMPT — Describe your app idea in plain English and Rork generates deployable, store-ready codePRICE — Standard Rork's paid plans start at $25/month: build with it first, then consider Max for native featuresMAX — Rork Max generates native Swift for every Apple platform, from iPhone to Vision ProNATIVE — It reaches native capabilities like AR/LiDAR, Metal 3D, Dynamic Island, Live Activities, and HealthKitPUBLISH — Publish to the App Store in two clicks; Rork Max is $200/monthEXPO — Standard Rork builds iOS and Android together via React Native (Expo) and is free to startPROMPT — Describe your app idea in plain English and Rork generates deployable, store-ready codePRICE — Standard Rork's paid plans start at $25/month: build with it first, then consider Max for native features
Articles/App Dev
App Dev/2026-06-21Advanced

Your List Jumps Back to the Top — Restoring Scroll Position Across Back Navigation and Process Death

How I rebuilt scroll restoration for a wallpaper grid by splitting it into two unrelated problems — back navigation and process death — covering getItemLayout, save timing, and killing the restore flicker.

Rork433FlatList9scroll positionstate restorationexpo9

Premium Article

You're swiping through a wall of wallpapers, you tap one to see it full size, and you press back. The list has snapped to the top, and your thumb goes hunting for the row you were just on. Running a few wallpaper apps as an indie developer, the usage data slowly makes it clear how much that small friction quietly costs you.

At first I thought the whole thing was one problem: "remember the scroll position on the way out." But once I started implementing it, I found the position gets lost in two situations with completely different causes. One is losing it while moving between screens. The other is losing it after the OS kills the app in the background and the user reopens it. Treat them as one problem and you only ever half-fix either.

Separate the two failures before writing any code

Draw the line first. If you start bolting on persistence without this split, you end up writing the offset to disk far too often and making scrolling janky.

SituationWhat is lostFix required
Push to detail, then back (within the stack)Nothing, in principle. Lost only on tab switches or conditional unmountsStop unmounting the list screen
Close and reopen the app (process alive)Nothing — in-memory state survivesNo work needed
OS kills it in the background → relaunchAll in-memory state is gonePersist the offset and restore it

So persistence is genuinely needed only for the third row. The first is a navigation-structure problem and is solvable with no storage at all. Confuse the two and you drift toward writing the position to disk on every frame.

Jumping to the top on "back" usually means the screen was destroyed

In a native-stack navigator, pushing the detail screen on top leaves the list screen mounted behind it. The FlatList's internal state is intact, so going back shows the same position — that is the default behavior.

When it still snaps to the top, the list screen is being unmounted somewhere. The two usual culprits:

The first is implementing a tab or segment switch with conditional rendering like condition ? <List/> : <Other/>. Every switch rebuilds <List/> from scratch and the scroll position resets to zero.

The second is swapping the whole list out while fetching with if (loading) return <Spinner/>. Each refetch destroys the list and re-renders it at the top.

The fix is simple: don't destroy the list.

// ❌ List is rebuilt on every switch
function Screen({ tab }: { tab: 'all' | 'favorites' }) {
  return tab === 'all' ? <WallpaperList /> : <FavoriteList />;
}
 
// ✅ Keep both mounted, toggle only visibility
function Screen({ tab }: { tab: 'all' | 'favorites' }) {
  return (
    <>
      <View style={{ flex: 1, display: tab === 'all' ? 'flex' : 'none' }}>
        <WallpaperList />
      </View>
      <View style={{ flex: 1, display: tab === 'favorites' ? 'flex' : 'none' }}>
        <FavoriteList />
      </View>
    </>
  );
}

Hiding with display: 'none' keeps the list mounted and preserves its scroll position. For two or three tabs this plain approach is perfectly serviceable. Do the same with the loading spinner: overlay it or render it as a list header instead of replacing the whole list, and the position holds.

That removes most of the back-navigation loss. What's left is the process-death case.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
Why a lost position on back-navigation and a lost position after process death are different bugs needing different fixes
Restoring before the first paint with getItemLayout so there is no top-of-list flicker
A low-cost save schedule: sample in onScroll, commit only on the move to background
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
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 →

Related Articles

App Dev2026-06-13
When and How to Remove Features Nobody Uses — Auditing and Safely Retiring Functionality in Rork-Built Apps
Unused features quietly make an app harder to maintain. A field-tested playbook from running six wallpaper apps in parallel — how to measure feature usage, decide what to retire, and remove functionality in three safe stages with Remote Config.
App Dev2026-06-07
expo start --offline Says 'forbidden'? Corporate Proxy (403) vs Dependency Validation
Two different failures make 'expo start --offline' or EXPO_OFFLINE=1 die with 'forbidden': an HTTP 403 from a corporate proxy, and Expo CLI validateDependenciesVersions guard. How to tell them apart, when to set HTTP_PROXY to route through the proxy, and when to go fully offline with pre-generated caches instead.
App Dev2026-06-03
Unifying Onboarding Across Six Wallpaper Apps: What One Month of First-Day Retention Showed Me
I folded the onboarding flows of six wallpaper apps scaffolded with Rork into a single config-driven component and watched first-day retention and push opt-in for a month. Here is an honest, operational note on what moved and what didn't.
📚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 →