After releasing v2.0.0 of Beautiful HD Wallpapers — an iOS/Android app I've been developing since 2014 with over 50 million cumulative downloads — I opened Google Play Console 28 days later to find the same crash pattern affecting more than 50 users. The stack trace was consistent. The cause turned out to be straightforward: I was mutating the array passed to the list component directly.
This exact problem surfaces in React Native FlatList just as readily as it does in Android's native RecyclerView. If your Rork-generated app crashes with Inconsistency detected or Cannot read properties of undefined during fast scrolling or rapid UI interactions, this is very likely what's happening. Here's what I found and how I fixed it in v2.1.0.
Why FlatList Breaks Under Direct Mutation
In JavaScript, arrays and objects are passed by reference. Writing const newList = items does not create a new array — it creates a new variable that points to the same object in memory.
// Looks harmless, but this is dangerous
const handleCategoryChange = (newCategory) => {
const filtered = allWallpapers; // reference copy, not a new array
filtered.sort((a, b) => a.category.localeCompare(b.category));
setWallpapers(filtered); // React may skip the update — same reference detected
};FlatList internally tracks item counts and indices to manage efficient rendering. When the array contents change but the reference stays the same, FlatList's internal snapshot drifts out of sync with the actual data. Fast scrolling or rapid taps force a reconciliation that exposes this inconsistency as a crash.
What makes this pattern particularly difficult to track down is that it rarely reproduces in development. You'll see it in production, on real devices, during real usage patterns. Device performance, network latency, and user interaction speed all affect the timing. You can end up in a situation where you see no crashes in Simulator but Play Console is accumulating them steadily.
Rork-generated code is well-structured out of the box. The problem typically enters when you edit or extend that code — adding an API call, implementing a delete feature, or sorting data after fetching it.
The Fix: Always Create a New Array
The rule is simple: before calling a State setter, always produce a new array reference. There are three main ways to do this: using the spread operator, using methods that return new arrays naturally (like filter and map), or using the functional update form of the setter.
// ✅ Spread operator creates a new array reference
const handleCategoryChange = (newCategory) => {
const filtered = [...allWallpapers].sort(
(a, b) => a.category.localeCompare(b.category)
);
setWallpapers(filtered); // New reference — React detects the change reliably
};
// ✅ Functional update receives the latest State value at call time
const addItem = async () => {
const newItem = await fetchItem();
setItems(prev => [...prev, newItem]);
};
// ✅ filter and map return new arrays — safe to use directly
const removeItem = (targetId) => {
setItems(prev => prev.filter(item => item.id !== targetId));
};One important distinction: [...array].sort() and array.sort() behave differently. .sort() sorts the original array in place. Always spread first, then sort. The same applies to splice and push — both modify the original array and should be avoided in favor of their non-destructive equivalents.
Three Patterns That Slip Into Rork-Generated Code
When editing code that Rork generates, these three patterns are easy to introduce. They're the React Native equivalents of what caused the crash in my wallpaper app.
Pattern 1: Destructive operations with splice or push
When adding a delete feature, splice is intuitive but problematic.
// ❌ splice mutates the original array
const deleteItem = (index) => {
const newItems = items; // same reference
newItems.splice(index, 1); // original array is modified
setItems(newItems); // React sees no reference change; UI may not update
};
// ✅ Use filter to produce a new array
const deleteItem = (index) => {
setItems(prev => prev.filter((_, i) => i !== index));
};Using filter returns a brand-new array containing only the elements that pass the condition. The original array is untouched. The same approach applies to sorting, reordering, and any other transformation.
Pattern 2: Shallow copy leaks with Object.assign
When updating a specific field inside a list item, Object.assign modifies the original object even though it looks like it's creating something new.
// ❌ Object.assign modifies the target object in place
const updateItem = (id, changes) => {
setItems(prev => prev.map(item =>
item.id === id ? Object.assign(item, changes) : item
));
};
// ✅ Spread creates a new object without touching the original
const updateItem = (id, changes) => {
setItems(prev => prev.map(item =>
item.id === id ? { ...item, ...changes } : item
));
};{ ...item, ...changes } copies every property from item into a fresh object, then overlays the properties from changes. The original item object is never touched. This is the standard React immutable update pattern and should be the default approach whenever you update fields inside list items.
Pattern 3: Stale closure in async operations
When fetching data asynchronously and appending it to an existing list, directly referencing the items variable captures its value at the time the async function began — not when it resolves.
// ❌ items is the value from when the async function started
// Any update that happened during the fetch is lost
const loadMore = async () => {
const result = await fetchNextPage();
setItems([...items, ...result.data]);
};
// ✅ Functional update always receives the latest State value
const loadMore = async () => {
const result = await fetchNextPage();
setItems(prev => [...prev, ...result.data]);
};By passing a function to the setter, React calls it with the current State value at the moment of the update — not a captured earlier value. For any async operation that touches State, this form should be the default.
Results After the Fix and How to Scan Your Project
After applying the defensive copy pattern throughout Beautiful HD Wallpapers v2.1.0, the crashes that had been hitting 50+ users over 28 days dropped to near zero within the first week post-release. The Android Vitals graph for user-impacting crashes was flat for the entire post-release measurement window.
To scan for the same issue in your Rork project:
# Find array variables assigned by reference before being passed to a setter
grep -rn "const new.*= items\|const new.*= data\|const new.*= list" src/ --include="*.ts" --include="*.tsx"
# Find destructive array methods used in component files
grep -rn "\.splice(\|\.push(\|\.sort(" src/ --include="*.tsx" | grep -v "// ✅"Work through the results and check whether any found code path modifies the array before calling a State setter. Where you find a problem, the fix is either to spread the array first or switch to the functional setter form.
Related Articles
For performance issues after fixing crashes, Migrating from FlatList to FlashList v2 in Rork covers the next optimization step.
If State updates are not reflecting on screen even after fixing mutation issues, State Not Updating or Not Re-rendering in Rork addresses the closely related problem of React's shallow comparison and when it fails to trigger a re-render.
One Thing to Do Now
Open your current Rork project and search for State setter calls that don't use setXxx([... or setXxx(prev =>. Each one is a candidate crash site.
Once this pattern is internalized, it becomes a natural checkpoint in every code review. The same defensive copy habit that prevents FlatList crashes also eliminates the stale closure bugs that are notoriously difficult to trace in async-heavy React Native code. The two problems share a root cause: treating mutable references as if they were values.