You save a file. Nothing happens. The terminal looks alive, but your app is still showing the state from five minutes ago. If you have spent any real time with a Rork-generated Expo project, you know this feeling. What makes it frustrating is that the cause is almost never the same twice. Sometimes it is a stale cache, sometimes it is node_modules, sometimes it is Watchman hitting its file-watch ceiling.
Metro bundler and Fast Refresh issues are part of the React Native landscape. This article walks through the symptoms I keep running into, and the order in which to check things so you can get back to work in minutes rather than hours. I want to go beyond "clear the cache and pray" and actually explain why each step matters.
The Three Signals to Check First
When a Fast Refresh issue hits, there is a strong urge to immediately run rm -rf node_modules. Resist it for 30 seconds. Check these three things first, and you will usually know where the problem lives.
First, look at the Metro terminal output. Are you seeing BUNDLE or Hot update messages when you save a file? If yes, Metro itself is running — the problem is on the device side (the simulator or phone is not receiving updates). If no, Metro itself is stuck.
Second, check the path of the file you edited. Is it inside your project root? Did you accidentally modify a file inside node_modules? Is it a symlinked file? Metro only watches specific paths, and edits outside those paths will never trigger Fast Refresh. This one catches people more often than you would think.
Third, look for warnings. If Fast Refresh tried to run but gave up, you will often see a message like "Fast Refresh had to perform a full reload" either in the terminal or in your editor's console. That message points to a structural issue in your component code (more on this below).
The Five Reasons Metro Actually Freezes
In my experience, Metro freezes almost always trace back to one of these five root causes.
Corrupted or Bloated Cache
Metro stores intermediate artifacts in .expo/ and /tmp/metro-*. When these get corrupted, you get stale bundles even after saving your code. This happens especially often when you edit Rork-generated SwiftUI code directly in Xcode — external file changes confuse Metro's change detection.
Watchman File-Watch Limits
On macOS, Watchman monitors file changes, but the default per-process limit is 8,192 files. Monorepos or projects with heavy dependency graphs blow past this easily, and any changes beyond that limit are silently ignored.
Mismatch Between node_modules and Lockfile
Mixing npm install and yarn install, or reinstalling dependencies under a different Node.js version, leaves your node_modules out of sync with your lockfile. Metro tries to carry on, but certain module resolutions hang indefinitely.
Expo SDK Version Drift
When the Expo SDK version in app.json no longer matches the expo package version in package.json, Metro starts fine but hangs partway through bundling. This is common right after you update your app in Rork but leave the local copy on the old version.
Port Conflicts
Metro defaults to port 8081. If another process is holding it, Metro either moves to a different port or fails to start. The device-side app is still pointing at 8081, so you get a "Metro is running but won't connect" situation.
Diagnostic Flow by Symptom
Symptom A: Saves do not update the screen
Check the Metro log. If BUNDLE messages appear, the issue is on the device side. Manually reload the app (⌘R in the iOS simulator, shake the device or open the menu on Android). If a manual reload shows the latest code, then only Fast Refresh is broken — suspect your component structure.
If there is no BUNDLE message, Metro cannot detect your file changes. Restart Watchman first, it is the fastest fix.
# Reset Watchman state
watchman watch-del-all
watchman shutdown-server
# Restart Metro with a clean slate
npx expo start --clearExpected output: after Starting Metro Bundler, you should see BUNDLE ./App.tsx. If it stalls here, move to the next step.
Symptom B: Metro hangs on "Loading dependency graph..."
Metro is stuck resolving dependencies. This is almost always a node_modules inconsistency. Clean up in a safe order:
# Step 1: Kill Metro and the simulator
# Ctrl+C to stop Metro, then quit the simulator
# Step 2: Remove caches and dependencies
rm -rf node_modules
rm -rf .expo
rm -f package-lock.json
# Step 3: Verify npm cache just in case
npm cache verify
# Step 4: Reinstall
npm install
# Step 5: Start with a cache clear
npx expo start --clearExpected output: a QR code with Metro waiting on exp://192.168.x.x:8081.
Symptom C: "Fast Refresh had to perform a full reload" keeps appearing
This is a code-structure problem, not a tooling problem. Fast Refresh only handles "pure updates" in React function components. These patterns force a full reload:
- Running side-effects at the top level of a file (like a bare
console.log) - Multiple
export defaultstatements - Mixing class and function components in the same file
- Anonymous functions as the default export (
export default () => {})
The anonymous default export is especially sneaky. Rork sometimes generates them, and simply naming the function often fixes Fast Refresh.
// ❌ Breaks Fast Refresh
export default () => {
return <View>...</View>
}
// ✅ Named function keeps Fast Refresh stable
export default function HomeScreen() {
return <View>...</View>
}The Full Reset Playbook (In Safe Order)
When nothing else works, here is the escalation ladder. Run each level, check if the issue is resolved, and only move on if it is not. You rarely need to go all the way.
#!/bin/bash
# rork-metro-reset.sh — escalate in safe order
# Level 1: Metro cache only (lightest)
npx expo start --clear
# Level 2: Reset Watchman too
watchman watch-del-all && watchman shutdown-server
npx expo start --clear
# Level 3: Blow away .expo and node_modules
rm -rf .expo node_modules
npm install
npx expo start --clear
# Level 4: Delete the lockfile to fully re-resolve
rm -rf .expo node_modules package-lock.json
npm install
npx expo start --clear
# Level 5: In the iOS simulator: Erase All Content and Settings
# Level 6: Delete Xcode DerivedData
rm -rf ~/Library/Developer/Xcode/DerivedData/*Levels 3 and beyond mean long reinstalls, so you really want Levels 1 or 2 to do the job. In my own setup, Level 2 resolves about 80% of the issues I run into.
Preventing Recurrence with Better Project Hygiene
If the same symptom keeps coming back every week, the project setup itself is worth revisiting.
Make sure .expo/ is in .gitignore. On a team, one person accidentally committing their cache directory can poison every other developer's Metro instance.
Pin your Node.js version. Drop an .nvmrc at the project root so everyone installs against the same Node.js build. Subtle node_modules differences from version drift are a surprisingly common Metro-stall trigger.
# .nvmrc
20.11.0
Raise the Watchman limits on large projects. You can increase the number of watchable files on macOS with:
echo "kern.maxfiles=524288" | sudo tee -a /etc/sysctl.conf
echo "kern.maxfilesperproc=524288" | sudo tee -a /etc/sysctl.confA restart applies the changes. For heavy Expo apps or projects leaning on react-native-reanimated and other file-heavy libraries, this headroom pays off.
If you are hitting issues outside the Metro layer, the Expo dependency error fix guide and the complete build error guide cover the adjacent ground.
One Concrete Next Step
Metro problems are annoying precisely because they are hard to reproduce. To future-proof yourself, consider adding scripts/reset.sh to your project today, containing Levels 1 through 3 from above. When the next stall hits, you will be back at your editor in under a minute instead of breaking flow to recall the exact commands. Protecting momentum matters as much as the raw speedup. In my own workflow, this small addition has meaningfully reduced Metro-related stress.