React Native not detecting changes: fix Fast Refresh and Metro

You save a file and nothing happens. The app shows old code, or only a manual reload picks it up. Work through the checks below in order, from the quick ones to the rare ones.

Symptoms

  • You edit a component and the screen does not change until you reload the app.
  • Metro prints no "BUNDLE" or "Reloading" lines when you save.
  • The app updates once, then stops updating.
  • Fast Refresh resets state on every edit, or does a full reload each time.
  • Native changes (a new permission, an icon, a config plugin) have no effect.

Quick checks first

  1. Is Fast Refresh on? Open the Dev Menu (shake the device, Cmd+D on iOS Simulator, Cmd+M or Ctrl+M on Android emulator, or press m in the Expo terminal) and make sure Fast Refresh is enabled.
  2. Is Metro running, and is it the right one? Check the terminal. Metro listens on port 8081 by default. If another project's Metro already uses that port, your app may be connected to the wrong bundler. Stop every Metro process and start the one for this project.
  3. Is a real device connected to Metro? On an Android device over USB, run adb reverse tcp:8081 tcp:8081. On Wi-Fi, the phone and the computer must be on the same network, and a firewall must not block port 8081.
  4. Are you running a debug build? Fast Refresh only works in development builds. A release build loads the JavaScript bundle embedded in the app and never talks to Metro.

Clear the Metro cache

A stale cache is the most common cause after changing branches, dependencies, or Babel and Metro config. Start the bundler with a clean cache:

# React Native CLI
npx react-native start --reset-cache

# Expo
npx expo start -c

If Metro still misses edits, clear Watchman too. Watchman is the file watcher Metro uses on macOS and Linux when it is installed:

watchman watch-del-all
watchman shutdown-server

As a last step, delete node_modules, reinstall, and start again with --reset-cache.

File watcher problems

Linux: inotify limit reached

On Linux, Metro can fail to watch a large project with errors like ENOSPC: System limit for number of file watchers reached, or silently stop seeing changes. Raise the limit:

echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

macOS: Watchman

Installing Watchman (brew install watchman) makes file watching more reliable on large projects. If it is installed but stuck, the watchman watch-del-all command above resets it.

Files outside the project root, symlinks and monorepos

Metro only watches the project root by default. Edits in a shared package outside it, for example in a monorepo, are not detected. Add the extra folders in metro.config.js:

const path = require('path');
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');

const workspaceRoot = path.resolve(__dirname, '../..');

module.exports = mergeConfig(getDefaultConfig(__dirname), {
  watchFolders: [workspaceRoot],
});

In an Expo project, use getDefaultConfig from expo/metro-config instead. Recent Expo SDKs configure monorepo watching automatically. After changing metro.config.js, restart Metro with a reset cache.

When Fast Refresh does a full reload or resets state

Fast Refresh works per file, and it has rules. Edits can look like they are "not detected" when it falls back to something else:

  • The file exports more than React components. If a file also exports constants or helper functions that other files import, Fast Refresh reloads the whole app instead of patching the component. Move non-component exports to another file.
  • Anonymous components. export default () => ... has no name, so state can be lost. Give the component a name.
  • Class components. Fast Refresh always remounts them and resets their state. Function components with Hooks keep state across edits.
  • Hooks with dependencies re-run. During Fast Refresh, useEffect, useMemo and useCallback always run again and ignore their dependency lists. This is on purpose, not a bug.

If you need a full reset at any point, reload the app from the Dev Menu or press r in the Metro terminal.

Native changes need a rebuild

Metro only serves JavaScript. These changes never show up through Fast Refresh and need a new native build:

  • Changes in android/ or ios/, including Gradle files, Podfile and Info.plist.
  • Installing a library that contains native code.
  • Changes to app.json or app.config.js fields that affect native code, and config plugins.

For a bare project, run npx react-native run-android or npx react-native run-ios again (run pod install in ios/ first after native dependency changes). For an Expo project that generates its native folders with prebuild, regenerate and rebuild. --clean deletes android/ and ios/ first, so skip it if you edit those folders by hand:

npx expo prebuild --clean
npx expo run:android
npx expo run:ios

Expo Go and development build mismatch

Expo Go ships one SDK version. If your project uses a different SDK, or uses native modules Expo Go does not include, behavior can be inconsistent or the app fails to load. Run npx expo install --fix to align packages with your SDK. If you need custom native code, use a development build instead of Expo Go. A development build also needs a rebuild when native dependencies change. A red "React Native version mismatch" screen is a related problem; see how to check your React Native version.

Still not updating?

Rule out version conflicts, because a mismatched React Native, React or Metro version can break the bundler in ways that look like a refresh problem. The React Native Upgrade Checker compares your package.json with a target and lists conflicts.

Metro still ignores your edits after all of this? Send me the project details and I will look at it.

Get help with my bug

Sources