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:
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.
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:
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
enabledcannot 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: falseis 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>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.
| Prop | Default | What it does |
|---|---|---|
showFloatingButton | true | Draw the button at all. Off leaves the panel reachable through the hook above |
iconComponent | none | Renders in place of the built-in glyph. Given the resolved size; colour is yours |
size | 44 | Diameter of the button, in dp |
color | accent | Button fill |
iconColor | white | The 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.