Axonpack

App setup

The provider, the ready-made list, your own screen with usePreview, and a banner that says what runs.

Three pieces, all from the package's main entry:

PieceWhat it does
PreviewProviderHolds the config, reads the downloads and the bucket's list, and confirms each start
PreviewListThe ready-made screen for QA
usePreview()The same list and actions, for a screen of your own

The provider

Put it near the root of the app, around everything:

import { PreviewProvider } from '@axonpack/preview';
import { previewConfig } from './preview-config';

export default function Root() {
  return (
    <PreviewProvider config={previewConfig}>
      <App />
    </PreviewProvider>
  );
}

The config is on Settings and env files, and every field is on Reference.

Mount it on every start

Once the provider has mounted, it tells the native side that this start worked. The crash fallback waits for that: a preview that never gets that far is marked broken, and the next start loads the base bundle. So mount the provider on every start, also in builds where enabled is false, and not behind a login or a lazy screen.

enabled

enabled: true turns previews on. The package does no build-type or store detection: you decide per build. With false the provider never reads the downloads or asks the bucket. The list does not hide itself, so leave it out of builds where previews are off; usePreview().enabled tells you which.

A common setup is an internal build with previews on and a store build with them off, from one env value:

enabled: process.env.EXPO_PUBLIC_PREVIEWS === 'on',

It applies to debug builds too. A preview chosen in a debug build loads instead of Metro, and the list says Metro is bypassed. Back to base returns to Metro.

The ready-made list

import { PreviewList } from '@axonpack/preview';

export default function PreviewsScreen() {
  return <PreviewList style={{ flex: 1 }} />;
}

Put it on a screen QA can reach: a tab in internal builds, a settings row, or a hidden gesture. It shows:

  • Running: what this start runs (the base bundle or a preview), and Back to base.
  • The previews for this build, each with its title, app version, branch, author, date, size, changelog and state, and Download, Switch, Update or Delete.
  • Incompatible: downloads made for another native build, with Delete all.
  • Dev preview servers, in a debug build with that option on. See Dev preview servers.
StateMeaning
Not downloadedPublished for this build, not on the device
DownloadingWith a progress bar
DownloadedOn the device and up to date
Update availablePublished again since it was downloaded
UnpublishedStill on the device, but no longer in the bucket
BrokenIt stopped while starting, so the base bundle loaded

Switch downloads first when the preview is not on the device, then restarts into it. Deleting the preview that runs goes back to the base bundle first.

Theme and labels

The list follows the device's light or dark mode. Change any colour, the radius or the fonts, and any text:

<PreviewList
  theme={{ mode: 'dark', primary: '#16a34a', radius: 8, fonts: { bold: 'Inter-Bold' } }}
  labels={{ title: 'QA builds', backToBase: 'Back to the store build' }}
/>

Every token and label is on Reference. LIGHT_THEME, DARK_THEME and DEFAULT_LABELS are exported to build on.

Your own screen

usePreview() gives the same data and actions. It needs the provider above it.

import { usePreview } from '@axonpack/preview';
import { Button, FlatList, Text, View } from 'react-native';

export function MyPreviews() {
  const preview = usePreview();
  if (!preview.enabled) return null;
  return (
    <FlatList
      data={preview.items}
      refreshing={preview.loading}
      onRefresh={preview.refresh}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => (
        <View>
          <Text>{item.entry.title} ({item.status})</Text>
          <Button title="Switch" onPress={() => preview.switchTo(item.id)} />
        </View>
      )}
      ListFooterComponent={<Button title="Back to base" onPress={() => preview.switchTo(null)} />}
    />
  );
}

The fields and actions are on Reference.

A banner that says what runs

So QA always knows what they are looking at, and how to get back. useRunning() says what this start runs: a dev preview server, a preview, or the base bundle.

import { useRunning } from '@axonpack/preview';
import { Text, View } from 'react-native';

export function BundleBanner() {
  const running = useRunning();
  if (running.kind === 'base') return null;
  const label =
    running.kind === 'preview'
      ? `Preview: ${running.title}`
      : `Dev server: ${running.server?.title ?? running.hostPort}`;
  return (
    <View style={{ backgroundColor: '#1d4ed8', padding: 8 }}>
      <Text style={{ color: 'white' }}>{label}</Text>
    </View>
  );
}

Render it inside the provider, above your navigation. The example app shows one on every tab.

Example app

The repository's example/ is the default create-expo-app template with the list on a Previews tab and a banner on every tab. It also opens links that run the same code as the list's buttons, for scripting a run from a terminal: previewexample://download/<id>, switch/<id>, switch/embedded, refresh, delete/<id>, deleteincompatible, local/<port> and localstop.

Next: Dev preview servers

On this page