Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions content/docs/expo-devtools/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,64 @@ description: Every published release of @axonpack/expo-devtools, newest first.

Every published release, newest first. The current version is **2.5.4**.

## 3.0.0

<ReleaseMeta pending bump="major" />

- The whole list of what moved where is in
[Upgrading](https://axonpack.github.io/docs/expo-devtools/upgrading).

**⚠️ Breaking Changes**

- Setting the devtools up is now one provider around your app. `createDevtoolsClient`, `init()` and `<DevtoolsOverlay />` are gone, and there is no client to create or pass anywhere:

```tsx
// before
export const devtools = createDevtoolsClient({ defaultTheme: 'dark' });
if (__DEV__) devtools.init();

<>
<YourApp />
{__DEV__ && <DevtoolsOverlay />}
</>;

// after
<DevtoolsProvider config={{ enabled: __DEV__, defaultTheme: 'dark' }}>
<YourApp />
</DevtoolsProvider>;
```

- **`enabled` is back, and it is the only gate.** With it off the provider patches nothing, records nothing and draws no button, so the mount can stay in a release build unguarded. It is read on the first render, so it cannot be changed later in the session.

- **An in-app browser takes one hook.** `useDevtoolsWebView` returns every prop the `<WebView>` needs, in place of the four client helpers:

```tsx
// before
<WebView
ref={devtools.getWebViewRef('checkout')}
userAgent={devtools.getWebViewUserAgent()}
injectedJavaScriptBeforeContentLoaded={devtools.getWebViewInjectedJavaScriptBeforeContentLoaded(
'checkout'
)}
onShouldStartLoadWithRequest={devtools.shouldAllowWebViewRequest}
onMessage={devtools.handleWebViewMessage}
/>;

// after
const devtoolsWebView = useDevtoolsWebView('checkout');
<WebView {...devtoolsWebView} />;
```

- **`webviewSources` is gone.** A browser view's name is whatever you hand the hook, and it is only the label its rows carry. Drop the option; nothing needs declaring up front.

- **`mark`, `measure`, `clearMarks`, `clearMeasures`, `setCrashContext` and the stores** now come from the exported `devtools` object: `import { devtools } from '@axonpack/expo-devtools'`.

**✨ Features**

- **Open the panel from your own code.** `useDevtoolsPanel()` gives you `show`, `hide`, `toggle`, whether it is open, and whether the devtools are running at all.
- **Hide the floating button.** `showFloatingButton={false}` leaves the panel working and takes the button off your screens.
- **Everything is patched before your first screen mounts,** including whatever it requests as it appears.

## 2.5.4

<ReleaseMeta date="2 September 2026" bump="patch" npm="https://www.npmjs.com/package/@axonpack/expo-devtools/v/2.5.4" />
Expand Down
14 changes: 6 additions & 8 deletions content/docs/expo-devtools/console.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,18 @@ Your app's files are bundled as private closures, so nothing can reach an import
way a browser console reaches a page's variables. Anything you want to poke at by name, hand over in
`context`:

```ts
createDevtoolsClient({
console: { context: { store, queryClient } },
});
```tsx
<DevtoolsProvider config={{ console: { context: { store, queryClient } } }}>
```

It is also the only thing that works in a release build, where the module list the two helpers above
read is not available.

<Callout type="warn" title="The prompt is on by default, in every build">
`console.repl` defaults to `true`, and it is not gated on `__DEV__`. Once `init()` has run, the
prompt is there — including in a release build, where it will run whatever is typed into it. Guard
your `init()` call (see [Leaving it in production](/docs/expo-devtools/production)), or turn the prompt off
explicitly with `console: { repl: false }`.
`console.repl` defaults to `true`, and it is not gated on `__DEV__`. Wherever the devtools are on the
prompt is there — including in a release build, where it will run whatever is typed into it. Ship
with `enabled: false` (see [Leaving it in production](/docs/expo-devtools/production)), or turn the prompt
off explicitly with `console: { repl: false }`.
</Callout>

## Limits
Expand Down
42 changes: 22 additions & 20 deletions content/docs/expo-devtools/crash-reporting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,25 @@ Which tier caught a crash decides how much it can say.

## Reporting from a release build

Crash capture has the only gate that is not `init()`. Setting one flag installs the handlers when the
client is **constructed**, so an app can keep its usual development-only `init()` call and still report
crashes from release:
Crash capture has the only gate that is not `enabled`. Setting one flag installs the handlers even
with the devtools off, so an app can keep its usual `enabled: __DEV__` and still report crashes from
release:

```ts title="devtools.ts"
export const devtools = createDevtoolsClient({
export const devtoolsConfig = {
enabled: __DEV__,
crash: { enableWhileDevtoolsDisabled: true },
});
} satisfies DevtoolsConfig;
```

On its own that captures **native exceptions only** — the crashes that end the app — and reports them
in the compact sheet. A later `init()` upgrades it: the JS tiers install too and the full sheet takes
With the devtools off that captures **native exceptions only** — the crashes that end the app — and
reports them in the compact sheet. With them on, the JS tiers install too and the full sheet takes
over. It brings nothing else with it either way: no panel, no REPL, no console capture, no request
bodies.

The JS tiers are held back before `init()` on purpose. They report errors the app survived, which is a
developer's concern, and the sheet there is in front of somebody using the app. A fatal JS error still
arrives, because React Native turns it into a native exception on its way to killing the process.
The JS tiers are held back on purpose. They report errors the app survived, which is a developer's
concern, and the sheet there is in front of somebody using the app. A fatal JS error still arrives,
because React Native turns it into a native exception on its way to killing the process.

<Callout type="info" title="Two sheets, and the wrong one in release is a real problem">
`popupDetail` defaults to `'auto'`, which picks between two sheets. With the devtools enabled you get
Expand All @@ -56,8 +57,8 @@ If you ship crash reporting without the panel, mount the sheet yourself:
import { CrashReportOverlay } from '@axonpack/expo-devtools';
```

`<DevtoolsOverlay />` already mounts one, and mounting both is harmless: whichever mounted first owns
the sheet and the other draws nothing.
`<DevtoolsProvider>` already mounts one, with the devtools on or off, and mounting both is harmless:
whichever mounted first owns the sheet and the other draws nothing.

## Catching render errors

Expand Down Expand Up @@ -89,13 +90,14 @@ Everything you pass is attached to every record from that point on.

To rewrite or drop a record before it is stored, handed to `onCrash` or written to disk, use `redact`:

```ts
createDevtoolsClient({
crash: {
redact: (record) => (record.message.includes('token') ? null : record),
onCrash: (record) => myBackend.send(record),
},
});
```tsx
<DevtoolsProvider
config={{
crash: {
redact: (record) => (record.message.includes('token') ? null : record),
onCrash: (record) => myBackend.send(record),
},
}}>
```

## Decisions worth knowing
Expand Down Expand Up @@ -128,6 +130,6 @@ createDevtoolsClient({
## Next step

<Cards>
<Card title="Client reference" href="/docs/expo-devtools/reference/client#crash" description="Every crash option and its default." />
<Card title="Provider reference" href="/docs/expo-devtools/reference/provider#crash" description="Every crash option and its default." />
<Card title="Leaving it in production" href="/docs/expo-devtools/production" description="What ships, and what stays dark." />
</Cards>
8 changes: 4 additions & 4 deletions content/docs/expo-devtools/debug.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ The two are not the same event, and the difference is worth seeing once. A JS th
reported before you let go of the button, while a main-thread crash ends the process and is read back
off disk at the next launch. Either way the report is waiting on the Crashes tab.

<Callout type="error" title="These buttons are not gated on __DEV__ or on init()">
They call straight into the native module, so they work whenever the panel is on screen, whether or
not `.init()` ran. Guarding the `<DevtoolsOverlay />` mount is what keeps them out of a release
see [Leaving it in production](/docs/expo-devtools/production).
<Callout type="error" title="These buttons are not gated on __DEV__">
They call straight into the native module, so they work whenever the panel is on screen.
`enabled: false` is what keeps them out of a release, because it is what makes the panel
unreachable — see [Leaving it in production](/docs/expo-devtools/production).
</Callout>

There is no record button and nothing to clear, so the tab carries no toolbar.
Expand Down
4 changes: 2 additions & 2 deletions content/docs/expo-devtools/example-app.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ bun run ios # or: bun run android (full native build)
## A worked configuration

`example/devtools.ts` doubles as a worked configuration: a dark default theme, a custom `midnight` one,
two declared `webviewSources`, a `console.context` you can reach from the prompt, and all four storage
adapters registered against real AsyncStorage, MMKV, SecureStore and an in-memory `Map`.
a `console.context` you can reach from the prompt, and all four storage adapters registered against
real AsyncStorage, MMKV, SecureStore and an in-memory `Map`.

<Callout type="info" title="MMKV is not in Expo Go">
AsyncStorage and SecureStore ship inside Expo Go, so `bun run start` exercises them as-is. MMKV does
Expand Down
70 changes: 39 additions & 31 deletions content/docs/expo-devtools/in-app-browsers.mdx
Original file line number Diff line number Diff line change
@@ -1,59 +1,67 @@
---
title: In-app browsers
description: Two props on the WebView, one declared name, and the page's requests and logs join your app's.
description: One hook on the WebView, and the page's requests and logs join your app's.
---

A `<WebView />` runs its own separate JavaScript, in a separate engine, invisible to everything that
patches `fetch` and `console` in your app. So it needs two props wired up:
patches `fetch` and `console` in your app. So it needs wiring of its own, and one hook returns every
prop it takes:

```tsx
import { useDevtoolsWebView } from '@axonpack/expo-devtools';
import { WebView } from 'react-native-webview';
import { devtools } from './devtools';

<WebView
source={{ uri: 'https://example.com' }}
injectedJavaScriptBeforeContentLoaded={devtools.getWebViewInjectedJavaScriptBeforeContentLoaded(
'my-webview'
)}
onMessage={(event) => devtools.handleWebViewMessage(event)}
/>;
```

Declare the name up front, so a typo cannot silently swallow everything:
export function Checkout() {
const devtoolsWebView = useDevtoolsWebView('checkout');

```ts title="devtools.ts"
export const devtools = createDevtoolsClient({
webviewSources: ['my-webview'],
});
return <WebView {...devtoolsWebView} source={{ uri: 'https://example.com' }} />;
}
```

That covers both the page's **requests and its console output**. Rows show up tagged
`WebView::[my-webview]` in either tab, and the Source chips can filter them apart from your app's own.
`WebView::[checkout]` in either tab, and the Source chips can filter them apart from your app's own.

`webviewSources` uses a TypeScript `const` type parameter, so the literal names flow into the helpers'
parameter types: passing an undeclared name is a compile error, and at runtime a message from an
undeclared source is dropped.
The name is yours to pick and it is only a label. Name each WebView when the app has more than one;
a single WebView can call the hook with no argument, which labels it `webview`.

<Callout type="warn" title="Use injectedJavaScriptBeforeContentLoaded, not injectedJavaScript">
The latter runs after the page's own scripts have already fired, so their requests escape.
<Callout type="warn" title="Leave injectedJavaScriptBeforeContentLoaded to the hook">
Setting your own replaces the instrumentation, and the page's early requests escape. Your own script
belongs in `injectedJavaScript`, which runs later.
</Callout>

## Optional: reaching the page with throttling
## What the props do

Three more props, only needed if you want the connection settings to apply to the page too:
| Prop | What it buys |
| --------------------------------------- | ---------------------------------------------------------------------------- |
| `injectedJavaScriptBeforeContentLoaded` | The page's `fetch`, `XMLHttpRequest`, `WebSocket`, `EventSource` and `console` |
| `onMessage` | Receives what the page reports. Without it nothing arrives |
| `ref` | A conditions change reaches an already-open page |
| `userAgent` | The browser override in Network conditions applies for real |
| `onShouldStartLoadWithRequest` | Navigation is blocked while Offline is on |

| Prop | Value | What it buys |
| ------------------------------ | ------------------------------------ | --------------------------------------------------------- |
| `ref` | `devtools.getWebViewRef('my-webview')` | A speed change reaches an already-open page |
| `userAgent` | `devtools.getWebViewUserAgent()` | The browser override applies for real |
| `onShouldStartLoadWithRequest` | `devtools.shouldAllowWebViewRequest` | Navigation is blocked while Offline is on |
Everything the hook returns is inert until the devtools are running: the injected script is empty, and
with no `onMessage` behind it `react-native-webview` does not install the page bridge at all.

A page can never be *fully* throttled: images, stylesheets and scripts the browser loads by itself
still go out at full speed.

## If the page uses postMessage for your own purposes

Take the handler out and call it first. It returns `true` when the message was one of this package's:

```tsx
<WebView
{...devtoolsWebView}
onMessage={(event) => {
if (devtoolsWebView.onMessage(event)) return;
handleMyOwnMessage(event);
}}
/>
```

## Next step

<Cards>
<Card title="Network" href="/docs/expo-devtools/network" description="Where the page's requests land." />
<Card title="Client reference" href="/docs/expo-devtools/reference/client" description="All five WebView helpers." />
<Card title="Hooks reference" href="/docs/expo-devtools/reference/hooks" description="The hooks and the devtools object." />
</Cards>
4 changes: 2 additions & 2 deletions content/docs/expo-devtools/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ Two more guides cover things that are not tabs: [themes](/docs/expo-devtools/the
a handful of readings dark.
- **It depends on no storage library.** The Storage tab reads the stores _you_ register, which is why
adding this package cannot drag AsyncStorage or MMKV into your app.
- **`init()` is the only gate.** Ship the code freely: until you call it, nothing is patched,
observed or recorded, and the overlay draws nothing.
- **`config.enabled` is the only gate.** Ship the code freely: with it off, nothing is patched,
observed or recorded, and the provider draws nothing but your app.
- **It states its limits.** Every guide here ends with what the tab cannot measure and why, rather
than showing a number it had to invent.

Expand Down
1 change: 1 addition & 0 deletions content/docs/expo-devtools/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"---Reference---",
"reference",
"---Releases---",
"upgrading",
"changelog"
],
"defaultOpen": true
Expand Down
45 changes: 27 additions & 18 deletions content/docs/expo-devtools/production.mdx
Original file line number Diff line number Diff line change
@@ -1,40 +1,49 @@
---
title: Leaving it in production
description: Shipping the code is safe. Two switches decide whether anything runs.
description: Shipping the code is safe. One switch decides whether anything runs.
---

Shipping the code is safe. Until `.init()` runs, nothing is patched and nothing is recorded, so the
cost of leaving the package in a production bundle is the bundle size and nothing else.
Shipping the code is safe. There is one switch, `config.enabled`, and with it off nothing is patched
and nothing is recorded, so the cost of leaving the package in a production bundle is the bundle size
and nothing else.

There are two switches, and they do different jobs:
```tsx
<DevtoolsProvider config={{ enabled: process.env.EXPO_PUBLIC_APP_ENV !== 'prod' }}>
<YourApp />
</DevtoolsProvider>
```

- **Capture** — `if (DEVTOOLS_ENABLED) devtools.init();` patches `fetch`, `XMLHttpRequest` and
`console`. Skip it and nothing is ever recorded.
- **Access** — `{DEVTOOLS_ENABLED && <DevtoolsOverlay />}` draws the floating button. Skip it and there
is no way into the panel.
The mount can stay exactly where it is. With `enabled: false` the provider renders its children and
the crash sheet and nothing else:

`DEVTOOLS_ENABLED` is whatever condition you want, evaluated at runtime.
- **No capture.** The `fetch`, `XMLHttpRequest`, `WebSocket` and `console` patches are never
installed, and no store is ever read.
- **No access.** There is no launcher button, and `useDevtoolsPanel().show()` opens nothing. That hook
reports `enabled` so your own trigger can hide itself rather than open an empty panel.

`enabled` is whatever condition you want, evaluated at runtime.
`process.env.EXPO_PUBLIC_APP_ENV !== 'prod'` from the [Quick start](/docs/expo-devtools/quick-start) and
`__DEV__` are the two usual choices; anything else works too, including a value you fetch for a
specific user.

<Callout type="info" title="Guarding the overlay is belt and braces">
It hides itself until `init()` has brought the panel up, so skipping the `init()` call alone is
enough. Guarding both is still worth doing — it keeps the component out of the render tree entirely.
<Callout type="info" title="Read once, on the first render">
The patches are global and go in one time, so the config that first render saw is the one that
applies. `enabled` cannot be flipped mid-session, and rebuilding the config object later changes
nothing.
</Callout>

That also settles the [Debug tab](/docs/expo-devtools/debug), whose buttons call straight into the native
module and are **not** restricted to development builds. They live behind the panel, and the panel is
unreachable without `init()`.
unreachable with the devtools off.

## The two things that do run in production

- **Crash reporting**, if you asked for it. `crash: { enableWhileDevtoolsDisabled: true }` installs the
handlers when the client is constructed rather than at `init()`. It is the one subsystem meant to
survive into a release build — see [Crash reporting](/docs/expo-devtools/crash-reporting).
- **The `>` prompt**, if you called `init()`. `console.repl` defaults to `true` and is not gated on
`__DEV__`, so a build that calls `init()` gets a prompt that runs whatever is typed into it. Set
`console: { repl: false }` for any build where that is not what you want.
handlers even with `enabled: false`. It is the one subsystem meant to survive into a release build —
see [Crash reporting](/docs/expo-devtools/crash-reporting).
- **The `>` prompt**, in any build where the devtools are on. `console.repl` defaults to `true` and is
not gated on `__DEV__`, so a build with `enabled: true` gets a prompt that runs whatever is typed
into it. Set `console: { repl: false }` for any build where that is not what you want.

## Next step

Expand Down
Loading
Loading