●MAX — Rork Max generates native Swift for every Apple platform, from iPhone to Vision Pro●NATIVE — It reaches native capabilities like AR/LiDAR, Metal 3D, Dynamic Island, Live Activities, and HealthKit●PUBLISH — Publish to the App Store in two clicks; Rork Max is $200/month●EXPO — Standard Rork builds iOS and Android together via React Native (Expo) and is free to start●PROMPT — Describe your app idea in plain English and Rork generates deployable, store-ready code●PRICE — Standard Rork's paid plans start at $25/month: build with it first, then consider Max for native features●MAX — Rork Max generates native Swift for every Apple platform, from iPhone to Vision Pro●NATIVE — It reaches native capabilities like AR/LiDAR, Metal 3D, Dynamic Island, Live Activities, and HealthKit●PUBLISH — Publish to the App Store in two clicks; Rork Max is $200/month●EXPO — Standard Rork builds iOS and Android together via React Native (Expo) and is free to start●PROMPT — Describe your app idea in plain English and Rork generates deployable, store-ready code●PRICE — Standard Rork's paid plans start at $25/month: build with it first, then consider Max for native features
Add Long-Press Drag Reordering to Your Rork Favorites List Without the Jank
A practical walkthrough for retrofitting long-press drag reordering onto a Rork-generated favorites list: keeping re-renders down, respecting the worklet boundary, persisting the order, and avoiding ghost cards and scroll conflicts.
Letting users rearrange their favorites by hand is one of those features that sounds trivial until you build it. As an indie developer, I run a set of wallpaper apps on both the App Store and Google Play, and the moment I shipped a favorites feature, requests came in to "let me put the ones I use most at the top."
Ask Rork to "make the favorites draggable to reorder," and it usually produces something that looks right but feels wrong on a real device: cards lag behind your finger, or the order snaps back the instant you let go. The generated code tends to be a FlatList with a hand-rolled PanResponder bolted on top, and that is exactly where the trouble starts.
Let's fix it from the failure points inward. The example targets the React Native (Expo) output of Rork, but the same reasoning carries over to Rork Max's SwiftUI output when you add .onMove to a List.
The three walls you hit with drag reordering
Drag-to-reorder looks like "move a card to where the finger is," but three problems show up at once.
The first is dropped frames. If a re-render fires every frame as the finger moves, the JavaScript thread clogs and the card trails behind. To hold 60fps you have to run the drag movement on the UI thread (a worklet), not on the JS thread.
The second is duplicated state. The "visual order" during the drag and the "data order" you persist on release are easy to model as two separate things, and if you reconcile them sloppily, the order rolls back the moment the user lifts their finger.
The third is scroll conflict. In a vertical list, both dragging and scrolling are up-and-down gestures, so unless you decide which wins, scrolling breaks or the drag never starts.
Trying to handle all three with a custom PanResponder balloons the code and breaks across devices. I burned a few hours trying to push a hand-written version through, and my takeaway is that for solo development, leaning on a proven library is the sturdiest choice.
Choosing a library — draggable-flatlist is the pragmatic answer
There are roughly three options for a reorderable list. Here is how they trade off.
Option
Strengths
Watch out for
Custom PanResponder
Zero dependencies, full control
You own worklet-ization, scroll conflict, and device quirks
react-native-draggable-flatlist
Built on Reanimated/Gesture Handler, smooth, short to implement
It is a FlatList inside, so very large data needs extra tuning
FlashList + custom layer
Fast rendering for huge lists
You still write the drag gesture layer yourself
For a favorites list of tens to a few hundred items, react-native-draggable-flatlist is the shortest and most stable path. It uses react-native-reanimated and react-native-gesture-handler internally and runs the drag movement in a worklet, so you don't have to cross the dropped-frame wall yourself.
When adding it to an Expo-managed Rork project, install Expo-compatible versions.
# Install into an Expo project (use Expo-compatible reanimated/gesture-handler)npx expo install react-native-reanimated react-native-gesture-handlernpm install react-native-draggable-flatlist
After installing react-native-reanimated, don't forget to put its Babel plugin last in babel.config.js. If it is missing, worklets won't run and the drag simply won't respond.
// babel.config.js — the reanimated plugin MUST be last in the arraymodule.exports = function (api) { api.cache(true); return { presets: ['babel-preset-expo'], plugins: ['react-native-reanimated/plugin'], // ← must be last };};
✦
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
✦If drag reordering felt broken on your Rork favorites list, you can get a smooth long-press implementation working today
✦You'll be able to diagnose and fix the three classic failures: stutter while dragging, order snapping back on release, and fights with vertical scroll
✦You can drop in a minimal persistence pattern that keeps the user's order across app restarts
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.
Here is the smallest setup that runs. Wrap your app root in GestureHandlerRootView, and use DraggableFlatList inside it. Forgetting to wrap the root is the classic cause of "nothing happens when I drag," so check it first.
// App.tsx (or the root component Rork generated)import { GestureHandlerRootView } from 'react-native-gesture-handler';export default function App() { return ( // Without flex: 1 the list renders at zero height <GestureHandlerRootView style={{ flex: 1 }}> <FavoritesScreen /> </GestureHandlerRootView> );}
Now the favorites list itself. The key move is to wire the drag function you receive in renderItem to the long-press trigger.
// FavoritesScreen.tsximport { useState, useCallback } from 'react';import { Text, TouchableOpacity } from 'react-native';import DraggableFlatList, { RenderItemParams, ScaleDecorator,} from 'react-native-draggable-flatlist';type Favorite = { id: string; title: string };const INITIAL: Favorite[] = [ { id: 'w-001', title: 'Daybreak Sea' }, { id: 'w-002', title: 'Misty Cedar Path' }, { id: 'w-003', title: 'Autumn Ravine' },];export function FavoritesScreen() { const [data, setData] = useState<Favorite[]>(INITIAL); // Freeze renderItem with useCallback to avoid recreating it every render const renderItem = useCallback( ({ item, drag, isActive }: RenderItemParams<Favorite>) => ( // ScaleDecorator slightly enlarges the dragged card for a "grabbed" feel <ScaleDecorator> <TouchableOpacity onLongPress={drag} // long press starts the drag disabled={isActive} // disable taps while dragging style={{ padding: 20, backgroundColor: isActive ? '#eef3ff' : '#fff', }} > <Text style={{ fontSize: 16 }}>{item.title}</Text> </TouchableOpacity> </ScaleDecorator> ), [], ); return ( <DraggableFlatList data={data} keyExtractor={(item) => item.id} renderItem={renderItem} onDragEnd={({ data }) => setData(data)} // commit the order exactly once /> );}
That gives you the core flow — long press, drag, release to commit — running smoothly. onDragEnd fires exactly once when the finger lifts, so you can concentrate state updates into that single call. During the drag, a worklet moves the coordinates internally, and no JS-thread update like setData runs. That is the decisive difference from a hand-rolled version.
Keeping re-renders down — where worklet and JS switch
The trick to staying smooth is to never break the boundary of "stay entirely on the UI thread while dragging, and only return to the JS thread on commit." react-native-draggable-flatlist draws that boundary internally, but the way you write renderItem can wreck it.
The easy mistake is creating fresh functions and objects on every pass inside renderItem. The pattern below re-renders the whole list often and chips away at drag smoothness.
// Anti-pattern: an inline style object and an anonymous function are created// on every render. The cost grows with the number of items.renderItem={({ item, drag }) => ( <TouchableOpacity onLongPress={() => drag()} // new function each time style={{ padding: 20, backgroundColor: '#fff' }} // new object each time > <Text>{item.title}</Text> </TouchableOpacity>)}
The fix is unglamorous but effective. Freeze renderItem with useCallback, move style out into StyleSheet.create, and wrap the row component in React.memo.
In my wallpaper apps, a meaningful share of users have more than 200 favorites, and the first naive implementation visibly stuttered mid-drag. After memoizing the row and freezing renderItem, drag tracking felt clearly better on the same device (a mid-range phone a few generations old). It is not a flashy optimization, but the gain is reliable.
Persisting the order — surviving a restart
If the order resets after the user closes and reopens the app, the feature is pointless. Save the committed order to the device and restore it on launch. For small, frequently read data like favorites, react-native-mmkv — which reads synchronously — fits better than AsyncStorage.
// storage.ts — persist the order (an array of ids) in MMKVimport { MMKV } from 'react-native-mmkv';const storage = new MMKV();const ORDER_KEY = 'favorites.order.v1'; // version the key for forward compatibilityexport function saveOrder(ids: string[]): void { storage.set(ORDER_KEY, JSON.stringify(ids));}export function loadOrder(): string[] { const raw = storage.getString(ORDER_KEY); return raw ? (JSON.parse(raw) as string[]) : [];}
Store only the "array of ids." The favorite bodies (titles, image URLs) live in another source (an API or a local DB), so persisting just the order as a lightweight index is the easiest model to maintain.
On restore, reorder the bodies by the saved ids. The important part is not losing favorites added after the save, or stumbling over ids that were since deleted. The function below absorbs this by "respecting the saved order while sending unknown items to the end."
// Apply a saved order to the current data:// - items present in the saved order keep that order// - new items absent from the saved order are appended at the end// - ids in the saved order that no longer exist are naturally ignoredexport function applyOrder<T extends { id: string }>( items: T[], savedOrder: string[],): T[] { const byId = new Map(items.map((it) => [it.id, it])); const ordered: T[] = []; for (const id of savedOrder) { const hit = byId.get(id); if (hit) { ordered.push(hit); byId.delete(id); } } // Append the rest (= new items) in their original order for (const it of items) { if (byId.has(it.id)) ordered.push(it); } return ordered;}
On the screen, restore on launch and save in onDragEnd.
import { useEffect, useState, useCallback } from 'react';import { saveOrder, loadOrder, applyOrder } from './storage';// fetchFavorites() is your existing function that pulls favorite bodies from API/DBconst [data, setData] = useState<Favorite[]>([]);useEffect(() => { const items = fetchFavorites(); setData(applyOrder(items, loadOrder())); // apply the saved order for the initial render}, []);const handleDragEnd = useCallback(({ data }: { data: Favorite[] }) => { setData(data); saveOrder(data.map((it) => it.id)); // persist the order only}, []);
Now the rearranged order survives a restart. If you later want cross-device sync via iCloud or Google Drive, you can extend this by putting that id array on your sync layer (cross-device sync is a separate topic, so here we stay on on-device persistence).
Common pitfalls
Here are the failures you are most likely to meet once it starts running, with their causes.
If the order snaps back on release, you either forgot to pass the onDragEnddata into setData, or another useEffect overwrites it with the initial data. Run the initializing useEffect once with an empty dependency array ([]) so it never races the drag commit.
A "ghost card" — the card appearing doubled like an afterimage during the drag — happens when keyExtractor doesn't return a stable, unique key. Using the array index as the key means the key shuffles every time the order changes, so always return the item's own id.
If vertical scrolling stops working, your long-press threshold is too short and the start of a scroll is being mistaken for a drag. Widening activationDistance a little (for example activationDistance={10}) cleanly separates a light swipe (scroll) from a deliberate long press (drag).
If the drag is unresponsive on Android only, suspect the GestureHandlerRootView wrapper first. On iOS it sometimes works by accident even when the wrap is missing, while Android tends to go unresponsive — it shows up as a platform difference.
How far to take it
Drag reordering is a rabbit hole: drop-position previews, moving across sections, auto-scroll while dragging — you can keep building forever. But in the context of reordering favorites, if you can hit "smooth long-press drag, commit and save on release, survive a restart," you've absorbed almost everything users ask for.
Shipping on both the App Store and Google Play, I've found that "not breaking on an unexpected device" moves review scores more than extra feature depth. That's why I leave the gesture layer to a proven library and spend my own time on persistence and pitfall handling.
Start by porting this minimal implementation into your own favorites screen, then load around 200 dummy items on a real device and check drag tracking. If it holds up there, add persistence to finish — that order is the safe one. Thank you for reading.
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.