The screen is visible. You tap a button. Nothing happens. No crash, no loading spinner—just silence. If you've built apps with Rork, this "frozen" state is something you'll likely run into at least once.
What makes it tricky is that freezes are harder to diagnose than crashes. With a crash, you get an error message or a white screen with a stack trace. With a freeze, you're left guessing. The UI looks normal. The app is technically running. But nothing responds.
After debugging quite a few of these in Rork-generated apps, I've found that most freezes fall into one of four categories. This guide walks through each one with specific code examples and fixes you can apply immediately—or pass directly to Rork's AI for correction.
Cause #1: Heavy Processing Blocking the Main Thread
React Native runs UI rendering and JavaScript logic on a single main thread. When you put expensive computation on this thread, everything else—button responses, animations, scroll behavior—grinds to a halt.
Rork's AI will occasionally write data-processing logic synchronously in event handlers, especially filtering or sorting over large arrays. Here's what that pattern looks like:
// ❌ Problematic: This filter blocks the main thread
const handleSearch = (query) => {
const results = hugeDataArray.filter(item =>
item.name.includes(query) &&
item.description.includes(query) &&
item.tags.some(tag => tag.includes(query))
);
setFilteredData(results);
};For a few hundred items, you won't notice anything. But as the dataset grows past a few thousand, the UI visibly stalls for a fraction of a second each time the user types. The fix is to yield control back to the main thread before doing the heavy work:
// ✅ Fixed: Release the main thread before processing
const handleSearch = useCallback(async (query) => {
setLoading(true);
// setTimeout(0) yields to the main thread so the UI stays responsive
await new Promise(resolve => setTimeout(resolve, 0));
const results = hugeDataArray.filter(item =>
item.name.includes(query)
);
setFilteredData(results);
setLoading(false);
}, [hugeDataArray]);The setTimeout(resolve, 0) trick isn't a hack—it's a well-established pattern for breaking up synchronous work in JavaScript. It gives the event loop a chance to flush any pending UI updates before the filter runs.
When asking Rork's AI to apply this fix, be specific: "Refactor handleSearch to run asynchronously and show a loading indicator while filtering is in progress." A vague prompt like "make this faster" often produces changes that miss the root issue.
Cause #2: Infinite Loops from useEffect Dependencies
If your app feels fine at first but gradually slows to a halt—or if network requests keep firing continuously in Rork Companion's dev tools—look at your useEffect dependency arrays. An improperly configured effect can trigger an infinite render cycle that locks up the UI and floods your backend.
// ❌ Triggers an infinite loop
const [userData, setUserData] = useState(null);
useEffect(() => {
fetchUserData().then(data => setUserData(data));
}, [userData]); // Every setUserData call re-triggers this effectThe logic is circular: the effect fetches data, setUserData updates state, the state change triggers the effect again. This repeats indefinitely. Your app might feel fine briefly, then slow to a crawl as pending network requests accumulate.
// ✅ Run once on mount
useEffect(() => {
fetchUserData().then(data => setUserData(data));
}, []); // Empty array = run once when component mountsThe most common version of this mistake involves object or array dependencies. Because objects are compared by reference in JavaScript, a new object literal in a dependency array always triggers the effect—even if the data inside it hasn't changed:
// ❌ options object is re-created on every render
useEffect(() => {
fetchData(options);
}, [options]); // options = { limit: 20 } re-creates every render
// ✅ Extract the values you actually care about
const { limit, offset } = options;
useEffect(() => {
fetchData({ limit, offset });
}, [limit, offset]); // Only re-runs when these values changeIf you notice network requests firing continuously in Rork Companion's developer panel, this is the first thing to check.
For more on reading and understanding Rork-generated React Native code, Reading Rork's Generated Code: React Native Basics is a solid starting point.
Cause #3: Missing Loading State Causing Double Submissions
When an async operation is triggered by a button press, and that button remains enabled during the operation, rapid taps create multiple simultaneous requests. Depending on how your state management handles conflicting updates, this can produce behavior that looks exactly like a freeze.
// ❌ No protection against multiple taps
const handleSubmit = async () => {
const result = await submitForm(formData); // Can be called multiple times in parallel
navigate('/success');
};// ✅ Guard against concurrent submissions
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async () => {
if (isSubmitting) return; // Early exit if already running
setIsSubmitting(true);
try {
const result = await submitForm(formData);
navigate('/success');
} catch (error) {
console.error('Submit error:', error);
Alert.alert('Error', 'Something went wrong. Please try again.');
} finally {
setIsSubmitting(false); // Always reset, regardless of outcome
}
};
<TouchableOpacity
onPress={handleSubmit}
disabled={isSubmitting}
style={[styles.button, isSubmitting && styles.buttonDisabled]}
>
{isSubmitting
? <ActivityIndicator color="#fff" size="small" />
: <Text style={styles.buttonText}>Submit</Text>
}
</TouchableOpacity>The finally block is important here. If you only reset isSubmitting in the success path, an API error will leave the button permanently disabled for that session.
When prompting Rork to add this pattern: "Disable the submit button while the API call is in progress, show an ActivityIndicator spinner, and handle errors with an Alert. Use a finally block to ensure the button is always re-enabled." That level of specificity tends to produce complete, correct code in one pass.
Cause #4: Duplicate Keys in FlatList
If a long list of items causes sluggishness or an unresponsive screen, FlatList key configuration is worth examining. This is an easy issue to overlook because it doesn't throw an obvious error—it just makes things progressively slower.
// ❌ If item.id is undefined, every key becomes the string "undefined"
<FlatList
data={items}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <ItemCard item={item} />}
/>When keyExtractor returns "undefined" for every item—which happens when your API response doesn't include an id field, or when it uses a different field name like _id or uuid—React Native detects key collisions on every render cycle. The longer the list, the more expensive this gets.
// ✅ Guaranteed unique keys + performance tuning
<FlatList
data={items}
keyExtractor={(item, index) => item.id?.toString() ?? `item-${index}`}
renderItem={({ item }) => <ItemCard item={item} />}
removeClippedSubviews={true} // Unmount components scrolled off-screen
maxToRenderPerBatch={10} // Fewer items rendered per batch
windowSize={5} // Smaller render window
initialNumToRender={10} // Fewer items on initial render
/>The removeClippedSubviews prop alone can make a meaningful difference on lists with more than 50 items. It tells React Native to unmount components that are no longer visible, freeing up memory and reducing the rendering workload.
If your list items are complex components, wrapping them in React.memo can further reduce unnecessary re-renders:
// Prevent re-rendering when props haven't changed
const ItemCard = React.memo(({ item }) => {
return (
<View style={styles.card}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.body}>{item.body}</Text>
</View>
);
});Getting Better Fixes from Rork's AI
Once you've identified a likely cause, frame your prompt around it. Instead of "the app is freezing, please fix it," try:
"The search screen freezes briefly when filtering more than 500 items. Please refactor the filter function to run asynchronously using
setTimeout(0)and show a loading indicator during processing."
Including your hypothesis in the prompt consistently produces better results. The AI doesn't have to guess what's wrong—it can address the specific mechanism causing the problem.
For a broader look at performance optimization in Rork apps, Rork App Performance Optimization: The Complete Guide covers profiling tools, rendering strategies, and memory management in depth.
Diagnosing the Cause with Console Logs
If you're not sure which pattern you're dealing with, console.log() is your fastest tool. Add logs around the suspect code and watch them in Rork Companion's developer panel (bottom-right icon):
const handlePress = async () => {
console.log('Button pressed: starting');
setIsLoading(true);
console.log('Loading state: true');
try {
const data = await fetchData();
console.log('Data loaded:', data.length, 'items');
setItems(data);
} catch (error) {
console.error('Fetch error:', error.message);
} finally {
setIsLoading(false);
console.log('Loading state: false');
}
};Watch where the logs stop appearing. If you see "Button pressed: starting" but never "Data loaded," the issue is in the async call. If you see the sequence repeating endlessly without any user interaction, you're likely in a useEffect loop.
For a systematic approach to error handling more broadly, Error Handling and Crash Prevention Basics covers patterns for building more resilient apps from the start.
Preventing Freezes from the Start
All four causes share a common thread: code that lacks appropriate guards for timing, concurrent state updates, or rendering load. Rork's AI generates better, more defensive code when you're explicit in your prompts. Adding phrases like "always include loading state management," "handle errors with try/catch and finally," and "guard against double submissions" to your prompts makes a real difference in the code quality you get back.
When you know these four patterns, you'll spend less time wondering what's wrong and more time shipping. The next time a screen stops responding, you'll likely recognize the pattern within a minute—and have a fix ready just as quickly.