Debugging

Open the dev menu, inspect the component tree in React Native DevTools, and turn a release crash into readable frames.

Syntaxnpx expo start # then press j for React Native DevTools, m for the dev menu

Every development build of a React Native app carries a dev menu. It is the entry point to the debugging tools, and it is compiled out of release builds along with LogBox and React Native DevTools.

Opening it

On the iOS simulator press Ctrl + Cmd + Z, or Cmd + D. On an Android emulator press Cmd + M on macOS, or Ctrl + M, or run adb shell input keyevent 82. On a physical device, shake it. With the Expo dev server running you can also press m in the terminal.

React Native DevTools

Since React Native 0.76, React Native DevTools replaces the old Chrome remote debugger. Press j in the terminal, or pick Open DevTools from the dev menu. It requires Hermes, the default engine. The panels are Console, Sources and breakpoints, Memory for heap snapshots, Components, and Profiler. The Network and Performance panels need React Native 0.83 or later.

The Components panel is not a DOM inspector. There is no HTML element under a View, so what you see is the React tree and the native views it created. Select an element to highlight it on the device, and read or edit its props and state in the side panel.

Crashes you did not watch happen

A release stack trace names minified bundle offsets. Feed it to metro-symbolicate with the source map from the same commit to get file names and line numbers back.

Two traps: console.log calls left in a shipped bundle slow the JS thread, and a source map from a different build gives line numbers that are confidently wrong.

Example

Example · tsx
import { useEffect, useState } from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";

type DebugCounterProps = { label: string };

// __DEV__ is a React Native global: true in a development build, false in a
// release build, where the bundler strips this branch out entirely.
function debugLog(message: string, value: unknown) {
  if (__DEV__) {
    console.log(message, value);
  }
}

export default function DebugCounter({ label }: DebugCounterProps) {
  const [count, setCount] = useState(0);

  // Set a breakpoint on the next line in the Sources panel to stop here,
  // then read `count` in the scope pane.
  useEffect(() => {
    debugLog("count changed", count);
  }, [count]);

  return (
    <View style={styles.container}>
      <Text style={styles.label}>{label}</Text>
      <Pressable onPress={() => setCount((c) => c + 1)} style={styles.button}>
        <Text style={styles.buttonText}>Increment</Text>
      </Pressable>
      {/* A number is not a string, but it still has to live inside <Text>. */}
      <Text style={styles.count}>{count}</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, alignItems: "center", justifyContent: "center", gap: 12 },
  label: { fontSize: 18, fontWeight: "600" },
  button: { backgroundColor: "#1f6feb", paddingHorizontal: 16, paddingVertical: 10, borderRadius: 8 },
  buttonText: { color: "#ffffff" },
  count: { fontSize: 32 },
});

When to use it

  • A button does nothing on tap, and you want a breakpoint inside the handler rather than a log line.
  • A list row renders the wrong name, and you want to read the props React actually passed it.
  • A tester sends a crash log full of minified frames from a release build.
  • A third-party warning fires on every render and buries your own warnings.

More examples

Symbolicating an Android release crash

A release bundle is Hermes bytecode, so the frames a user's crash produces are offsets, not file names. metro-symbolicate maps them back. Reach for this when a tester sends you a crash log you cannot read. The source map must come from the exact commit that was built; a map from a later commit still resolves, but to the wrong lines.

Example · bash
# Capture the trace from the device, minified frames and all.
adb logcat -d > stacktrace.txt

# Translate it using the source map produced by the SAME release build.
npx metro-symbolicate \
  android/app/build/generated/sourcemaps/react/release/index.android.bundle.map \
  < stacktrace.txt

# Or pipe logcat straight through it.
adb logcat -d | npx metro-symbolicate \
  android/app/build/generated/sourcemaps/react/release/index.android.bundle.map

Silencing a known warning so real ones stay visible

A noisy third-party package can produce a warning on every render, which pushes your own warnings off the screen. LogBox.ignoreLogs filters by prefix string or regular expression. Use it for a warning you have read, understood and decided to live with, and nothing else.

Example · tsx
import { LogBox } from "react-native";

// Call this once, at module scope in the entry file, before anything renders.
if (__DEV__) {
  // Matches by prefix or by regular expression.
  LogBox.ignoreLogs([
    "Require cycle:",
    /Setting a timer for a long period/,
  ]);
}

// LogBox.ignoreAllLogs() exists too, and is almost always a mistake:
// it hides the yellow box that would have told you about the next bug.

Labelling a custom hook in the Components panel

By default the Components panel lists a custom hook's internal state as anonymous values, which is unreadable once a hook holds three or four of them. useDebugValue(value) attaches one readable label. Worth adding to hooks you inspect often, such as auth or connectivity.

Example · tsx
import { useDebugValue, useEffect, useState } from "react";
import NetInfo from "@react-native-community/netinfo";

export function useIsOnline(): boolean {
  const [online, setOnline] = useState(true);

  // Shows "IsOnline: online" next to the hook in the Components panel.
  // React strips useDebugValue from production builds.
  useDebugValue(online ? "online" : "offline");

  useEffect(() => {
    const unsubscribe = NetInfo.addEventListener((state) => {
      setOnline(state.isConnected === true);
    });
    return unsubscribe;
  }, []);

  return online;
}

Discussion

  • Be the first to comment on this lesson.