Axonpack

Quick start

Wrap your app in the provider once, at the root. That is the whole setup.

One thing has to happen: <DevtoolsProvider> wraps your app, once, at the root. It starts the devtools and hosts the panel. Nothing else.

1. Keep the config in its own file

One object the root imports, with the flag that decides whether any of this runs:

devtools.ts
import type { DevtoolsConfig } from '@axonpack/expo-devtools';

export const devtoolsConfig = {
  enabled: process.env.EXPO_PUBLIC_APP_ENV !== 'prod',
} satisfies DevtoolsConfig;

Set EXPO_PUBLIC_APP_ENV=prod for your production builds (in eas.json, or a .env file) and leave it unset everywhere else. Use __DEV__ instead if a dev/release split is all you need. An inline object on the provider works just as well; a file of its own only keeps a long config out of your root layout.

2. Wrap your app

Use whichever of these matches your app. You only need one.

The root layout is the place. One provider there covers every route.

app/_layout.tsx
import { Stack } from 'expo-router';
import { DevtoolsProvider } from '@axonpack/expo-devtools';
import { devtoolsConfig } from '../devtools';

export default function RootLayout() {
  return (
    <DevtoolsProvider config={devtoolsConfig}>
      <Stack />
    </DevtoolsProvider>
  );
}

That is it. Drag the button anywhere on screen, tap it to open the panel, and the Network and Console tabs are already recording. Navigation records too, once it has found your navigator. The Storage tab is empty until you tell it which stores you use. See Storage. The panel reopens on whichever tab you last had open, for as long as the app is running.

The Navigation tab finds Expo Router on its own. With React Navigation, put the provider inside your NavigationContainer and it finds that too. A provider above the container needs the container handed over with useDevtoolsNavigation. See Navigation.

3. Optional: the tabs on your computer

For the same tabs in React Native DevTools too, add one line to metro.config.js and restart Metro:

metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
const { withDevtools } = require('@axonpack/expo-devtools/metro');

module.exports = withDevtools(getDefaultConfig(__dirname));

Press j in Metro and the Axonpack tab is beside Console and Sources. React Native DevTools has the rest.

Things that trip people up

  • Mount the provider exactly once. The root is the place, because one mount there covers every route: the panel opens as a modal on top of whichever screen is showing, so nested Tabs and Drawer layouts are already covered and must not mount their own. A second provider gives you a second button.
  • The patches go in as the provider renders, not in an effect, which is earlier than any child's mount and so catches what the first screen requests. Earlier still is out of reach: anything during module evaluation, before React renders at all, happens before this package can see it.
  • The config is read once. The first render is what configures everything, because the patches are global and go in one time. Changing the object later has no effect, so enabled cannot be flipped at runtime.
  • Performance starts paused. Measuring is not free, so press its record button when you want it. The other recording tabs record from launch.
  • Expo Go works. See Installation for the handful of readings that go quiet there.
  • In-app browser pages need one hook on the <WebView /> itself. See In-app browsers.
  • enabled: false is the guard, and the mount can stay where it is: the provider then renders its children and nothing else, so there is no button, no panel and nothing patched. Crash reports can still surface, because that is the one subsystem meant to run in production. See Production.

Opening the panel without the button

The floating button is optional. Turn it off and open the panel from your own UI instead: a long-press on a header, a row in a staff-only settings screen, a gesture nobody will find by accident.

<DevtoolsProvider config={devtoolsConfig} showFloatingButton={false}>
  <YourApp />
</DevtoolsProvider>
SettingsRow.tsx
import { useDevtoolsPanel } from '@axonpack/expo-devtools';

export function SettingsRow() {
  const panel = useDevtoolsPanel();
  if (!panel.enabled) return null;

  return <Button title="Open devtools" onPress={panel.toggle} />;
}

useDevtoolsPanel() gives you visible, enabled, show(), hide() and toggle(). enabled is false in a build that never started the devtools, which is what lets your trigger take itself off the screen rather than open an empty panel. It reads the same state the button does, so the two stay in step.

The launcher button

Nothing has to be configured: the provider on its own gives you the bug glyph on the theme's accent colour. Everything about its appearance is a prop on the provider, since that is where you mount it.

PropDefaultWhat it does
showFloatingButtontrueDraw the button at all. Off leaves the panel reachable through the hook above
iconComponentnoneRenders in place of the built-in glyph. Given the resolved size; colour is yours
size44Diameter of the button, in dp
coloraccentButton fill
iconColorwhiteThe built-in glyph only; an iconComponent colours itself
statusBar'auto'Status bar icons while the panel is open: 'auto', 'app', 'light', 'dark'
<DevtoolsProvider
  config={devtoolsConfig}
  iconComponent={({ size }) => <MyLogo width={size} height={size} />}
  size={56}
  color="#111827">
  <YourApp />
</DevtoolsProvider>

A size under 44 still gets a 44dp touch area through hitSlop, so a small button stays as easy to hit as it looks, and however big you make it, the drag stays inside the screen.

Pick the two colours together

color and the default glyph have to work together: a pale button needs iconColor set, or the white glyph vanishes into it.

Next step

On this page