Axonpack
Reference

DevtoolsProvider

The provider's props, and every configuration option underneath them.

import { DevtoolsProvider } from '@axonpack/expo-devtools';

<DevtoolsProvider config={config}>
  <YourApp />
</DevtoolsProvider>;

Wrap your app in it once, at the root. It starts the devtools as it renders, which is before any child's mount, and hosts the panel. There is no init to call.

Props

PropTypeDefaultWhat it does
configDevtoolsConfig{}Everything below. Read once, on the first render.
showFloatingButtonbooleantrueDraw the launcher button. Off leaves the panel reachable through useDevtoolsPanel.
iconComponentComponentType<{ size: number }>noneRenders in place of the built-in glyph. Given the resolved size.
sizenumber44Diameter of the button, in dp.
colorstringaccentButton fill.
iconColorstring'#ffffff'The built-in glyph only; an iconComponent colours itself.
statusBar'auto' | 'app' | 'light' | 'dark''auto'What the status bar's clock and icons do while the panel is open.

The provider is generic over your theme names, so config.defaultTheme accepts a built-in id or a key of config.themes and nothing else.

The button is draggable, stays inside the screen, and keeps a 44dp touch area through hitSlop even at a smaller size. Rendering it is also what marks first render for the startup breakdown.

statusBar

The panel never paints a status bar background of its own: the header extends behind it, so that area is already the toolbar's colour. What this prop decides is the content.

ValueWhat happens
'auto'Follows the theme, each of which carries its own statusBarStyle. A dark theme gets light icons, a light one dark.
'app'Untouched, for an app that manages the status bar itself.
'light'/'dark'That content style whatever the theme is on. 'light' means light icons, for a dark background.

Whatever the app had is restored when the panel closes. Without this, a light app's dark icons stay dark and become unreadable over a dark panel.

iOS needs one Info.plist key

React Native's StatusBar cannot change the style unless UIViewControllerBasedStatusBarAppearance is false in Info.plist. An Expo app's own template already sets it, so there is usually nothing to do. If yours does not, statusBar has no effect on iOS and 'app' is the honest setting.

Top level

OptionTypeDefaultWhat it does
enabledbooleantrueWhether the devtools run at all. The only gate; see Leaving it in production.
defaultThemeThemeId'light'Which theme the panel opens with: a built-in or one of yours.
themesRecord<string, ThemeConfig>undefinedYour own themes: a base to inherit and the tokens to override.

With enabled: false the provider renders its children and the crash sheet and nothing else: no patches, no button, no panel. The config is read on the first render and never again, because the patches are global and go in one time, so enabled cannot be flipped mid-session.

Network

The switches name the kind of traffic, not the mechanism that carried it. A request is a request whether it went out through fetch, through XMLHttpRequest, from a JSI client or from inside a page.

OptionTypeDefaultWhat it does
network.httpbooleantrueCapture plain requests, by whatever transport they left on. Off also means no phase timing.
network.websocketbooleantrueCapture WebSocket connections and their messages, the app's own and a page's.
network.ssebooleantrueCapture server-sent event streams and their events, whichever client opened them.
network.disabledByDefaultbooleanfalseOpen the Network tab paused.
network.redactHeadersreadonly string[][]Header names whose values are stored as [redacted], matched without regard to case. Listing cookie also redacts a page's document.cookie.
network.redact(entry: NetworkEntry) => NetworkEntry | nullundefinedRuns after redactHeaders, each time an entry changes, before it is stored. An entry is an HTTP row (NetworkLogEntry) or a WebSocket row, told apart by kind. Return the entry, edited or not, or null to drop the row. If it throws, the row is dropped.

With sse off, the app's own stream is still recognised as one. Its endless body has to be, or it would be read as a response. So the row remains and only the events are dropped. A page's stream has no request underneath it that anything here can see, so that one disappears entirely.

Redaction happens before a request is stored, so nothing downstream sees the real value: not the list, the detail panel, copy, export, the DevTools tab or crash breadcrumbs. Replaying a request from the panel sends the placeholder too. redactHeaders covers HTTP rows only, while redact sees WebSocket rows too. Socket frames and stream events pass through neither. See Redaction.

network: {
  redactHeaders: ['authorization', 'cookie', 'set-cookie'],
  redact: (entry) => ({ ...entry, url: entry.url.replace(/token=[^&]+/, 'token=[redacted]') }),
}

Console

OptionTypeDefaultWhat it does
console.capturebooleantrueMirror console.* into the Console tab, including from WebViews wired with useDevtoolsWebView.
console.replbooleantrueShow the > prompt.
console.contextRecord<string, unknown>undefinedExtra names an expression can use, for example { store, queryClient }.
console.disabledByDefaultbooleanfalseOpen the Console tab paused. The prompt still works.

console.repl is not gated on __DEV__

It defaults to true in every build. Wherever the devtools are on the prompt is there, including in a release build, where it runs whatever is typed into it. Ship with enabled: false, or set console: { repl: false }.

Performance

OptionTypeDefaultWhat it does
performance.sampleIntervalMsnumber1000How often memory is sampled. Each read crosses into the engine, so keep it coarse.
performance.longTaskThresholdMsnumber150Only keep tasks that blocked the JS thread at least this long.
performance.interactionThresholdMsnumber100Only keep interactions at least this long, event to next paint.
performance.historySizenumber120How many memory samples, long tasks, user timings and interactions are kept.
performance.disabledByDefaultbooleantrueOpen the Performance tab paused. Defaults to on, since measuring costs something.

Storage

OptionTypeDefaultWhat it does
storage.adaptersStorageAdapterDefinition[]undefinedThe stores the Storage tab can see. Nothing is discovered automatically.
storage.maxKeysnumber1000Keys read per store before the tab stops and says how many it skipped.
storage.readOnlybooleanfalseBlanket read-only default; an individual adapter can still set its own.

See Storage adapters for how to build one.

OptionTypeDefaultWhat it does
navigation.disabledByDefaultbooleanfalseOpen the Navigation tab paused. The route on screen is still shown.
navigation.redact(move: NavigationMove) => NavigationMove | nullundefinedRuns on every move before it is stored, including the route shown as current. Return the move, edited or not, or null to drop it. If it throws, the move is dropped.

The tab is there only when a router is installed. Expo Router is found on its own. So is a React Navigation container the provider sits inside. A container mounted inside the provider is handed over with useDevtoolsNavigation.

What redact returns is all anything downstream sees: the history, the tree, copy, export and crash reports.

navigation: {
  redact: (move) =>
    move.to?.params && 'token' in move.to.params
      ? { ...move, to: { ...move.to, params: { ...move.to.params, token: '[redacted]' } } }
      : move,
}

See Navigation and the Navigation tab.

Crash

Crash capture is the only part of this package that runs with the devtools off.

OptionTypeDefaultWhat it does
crash.enabledbooleantrueCapture at all.
crash.enableWhileDevtoolsDisabledbooleanfalseInstall the handlers even with enabled: false, so a release build still reports crashes.
crash.handlers.jsErrorsbooleantrueThe ErrorUtils global handler, for fatal and non-fatal JS errors.
crash.handlers.unhandledRejectionsbooleantrueUnhandled promise rejections, via the Hermes rejection tracker.
crash.handlers.nativeExceptionsbooleantrueUncaught Java/Kotlin and Objective-C exceptions, via the native module.
crash.popupDetail'auto' | 'full' | 'compact''auto'Which sheet a crash opens. 'auto' is the full sheet when the devtools are enabled, compact when not.
crash.disableDefaultLogBoxbooleanfalseUninstall React Native's LogBox, so a JS error is reported here and nowhere else.
crash.breadcrumbsbooleantrueAttach the recent console, network and navigation entries to each record.
crash.maxRecordsnumber25Reports kept in memory.
crash.persistNonFatalbooleanfalseAlso write non-fatal records to disk.
crash.redact(record: CrashRecord) => CrashRecord | nullundefinedRuns before the record reaches the store, the disk or onCrash. Return null to drop it.
crash.onCrash(record: CrashRecord) => voidundefinedYour own handler, after redact.

With the devtools off, an app relying on enableWhileDevtoolsDisabled alone installs nativeExceptions only, whatever the other two say: the JS tiers report errors the app survived, which is a developer's concern, and the sheet there is in front of a user. A fatal JS error still arrives, because React Native turns it into a native exception on the way to killing the process.

disableDefaultLogBox uninstalls LogBox rather than muting it, which takes the yellow warning toasts with it. LogBox is one component and the two cannot be separated. Warnings are still captured by the Console tab. It only does anything in development; LogBox is already an empty stub in a release build.

ComponentWhat it is for
<CrashReportOverlay />The crash sheet on its own, for a build that ships crash reporting but not the panel. The provider mounts one itself, with the devtools on or off; mounting both is harmless.
<DevtoolsErrorBoundary />Catches render errors, the only tier that produces a component stack. Props: children, fallback(error, reset), onError(error, info).

On this page