Skip to content

Expo Guide

Language: JavaScript

Expo is the app framework for the Superloom client stack. This page documents what Expo provides, when to build a Superloom adapter versus using Expo directly, the capability injection pattern, prebuild and CNG, SDK versioning, and the cloud account features that require an Expo account.

On This Page


What Expo Provides

The Expo SDK bundles capabilities that would otherwise require separate native modules, manual linking, and platform-specific configuration. Each capability ships as a versioned package under the expo-* namespace.

CapabilityExpo packageWhat it handles
Fontsexpo-fontFont registration on native, @font-face injection on web
Assetsexpo-assetAsset loading, caching, local URI resolution
File systemexpo-file-systemRead, write, and manage files on device
Secure storageexpo-secure-storeEncrypted key-value storage on native
SQLiteexpo-sqliteLocal relational database
Notificationsexpo-notificationsPush notification registration and handling
Image pickerexpo-image-pickerCamera and photo library access
Cameraexpo-cameraCamera preview and capture
Locationexpo-locationGeolocation and permissions
Application infoexpo-applicationVersion, build number, installation metadata
Device infoexpo-devicePlatform, model, OS version
Screen orientationexpo-screen-orientationLock and detect orientation changes
Splash screenexpo-splash-screenNative splash screen control

These packages work in any React Native project via npx install-expo-modules. The distinction between Expo Go and prebuild concerns the dev workflow, not package consumption. A project that uses expo-font without Expo Go or prebuild still benefits from the package.


Adapter Versus Direct Use

The rule: if a capability is consumed inside a helper module, wrap it behind a capability-named injection slot. If a capability is consumed only in app code (screens, layouts, boot files), use the Expo API directly.

The reason is decoupling. A helper module that imports expo-font directly is bound to Expo in its source text. A helper module that receives a FontLoader capability through shared_libs works against any backend that satisfies the contract. The same module runs against Expo, a bare RN loader, or a test stub with no edit.

When to Wrap

ConditionAction
The capability is needed inside a helper moduleWrap behind a capability-named injection slot
The capability is needed inside a Class H extensionThe extension imports the Expo package directly; the parent stays pure
The capability is needed only in app codeUse the Expo API directly in the app

When a Pure Parent Exists

When a module's logic has a second consumer beyond Expo, the architecture splits into a pure parent (Class G) and an Expo extension (Class H). The parent holds the framework-free logic. The extension imports the Expo package and implements the adapter contract.

The extension is named [parent]-ext-expo. It imports the pure parent and adds Expo-specific code. The parent never imports Expo.

When No Second Consumer Exists

When Expo is the only consumer, a standalone module takes the js-rnw-helper-* prefix (Class I). The module imports the Expo package directly and exposes its API through the loader pattern. The decision test determines the shape before creation.


Capability Injection Pattern

Injection slots in shared_libs are named for the capability, never for the vendor that satisfies it.

CorrectIncorrectReason
shared_libs.FontLoadershared_libs.ExpoFontThe slot describes what it does, not what provides it
shared_libs.KeyValueStoreshared_libs.MMKVA vendor-named slot couples the module to that vendor through its own source text
shared_libs.AssetLoadershared_libs.ExpoAssetThe same module can swap backends without a source edit

The rule binds module code, test loaders, host manifests, and documentation examples equally. A test loader injects a stub with the same surface:

javascript
const FontLoader = {
  useFonts: () => [true, null]
};

The module calls shared_libs.FontLoader.useFonts() without knowing whether the backend is Expo, a bare RN loader, or a test stub. This is what makes the module testable in pure Node with no Metro and no emulator.

See Client Loader for how injection slots enter the Lib container, and Module Structure for the Class I and Class H loader patterns.


Prebuild and Continuous Native Generation

Prebuild generates native project directories (ios/ and android/) from the Expo app configuration (app.json or app.config.js). This is called Continuous Native Generation (CNG).

How CNG Works

  1. The app configuration in app.json declares native dependencies and plugins
  2. npx expo prebuild reads the configuration and generates ios/ and android/ directories
  3. The generated directories are gitignored and regenerated on demand
  4. Native builds (Xcode, Gradle) run against the generated directories
  5. npx expo prebuild --clean regenerates from scratch, discarding manual native edits

When to Use Prebuild

ScenarioPrebuild needed
Expo Go developmentNo
Dev client with custom native modulesYes
EAS Build for app store binariesYes (EAS handles it automatically)
Local npx expo run:ios or run:androidYes

Dev Client Versus Expo Go

Expo Go cannot run custom native modules. When a project includes native code beyond the Expo SDK, a dev client replaces Expo Go. The dev client is a custom build of the Expo runtime that includes the project's native dependencies.

bash
npx expo prebuild
npx expo run:ios     # Builds dev client and runs on iOS

The dev client preserves Expo's developer tools (hot reload, dev menu, error overlay) while supporting custom native code.


Expo SDK Versioning

The Expo SDK aligns with React Native releases. Each SDK version pins a specific React Native version and a set of compatible expo-* package versions.

Upgrading the SDK

bash
npx expo install expo@latest
npx expo install --fix

The first command upgrades the expo package. The second command upgrades all expo-* packages to versions compatible with the new SDK. This two-step process prevents version mismatches between the SDK and its packages.

Version Pinning

Pin the Expo SDK version in package.json. Do not use caret ranges for the expo package itself. The expo-* packages use caret ranges within a major SDK version.

json
{
  "dependencies": {
    "expo": "~51.0.0",
    "expo-font": "~13.0.0"
  }
}

Breaking Changes

Expo SDK upgrades can introduce breaking changes. The SDK changelog on Expo's documentation site lists the changes per version. Review the changelog before upgrading.


Expo Cloud Account Features

Local development requires no Expo account. The features in this section require a free or paid Expo account and depend on Expo's cloud infrastructure.

EAS Build

EAS Build compiles app binaries for iOS and Android in Expo's cloud. The service runs the native build (Xcode, Gradle) on Expo's servers and returns a signed binary.

FeatureAccount requirement
iOS buildsFree account (limited builds per month)
Android buildsFree account (more builds than iOS)
Priority buildsPaid plan
Custom build profilesFree account

iOS builds on the free tier are limited per month. Android builds have a higher allowance. Paid plans increase build volume and add priority queues.

EAS Update

EAS Update pushes JavaScript bundle updates to deployed apps without a new app store release. This is the OTA (over-the-air) update mechanism.

FeatureAccount requirement
Update publishingFree account
Update branching and channelsFree account
Production-scale concurrencyPaid plan
Update rollbackFree account

Updates published through EAS Update are signed and versioned. The client runtime checks for updates on app launch and applies them on the next restart.

EAS Submit

EAS Submit sends built binaries to Apple App Store Connect and Google Play Console. It automates the upload and metadata submission process.

FeatureAccount requirement
App Store submissionFree account
Play Store submissionFree account
Auto-submission on buildFree account

EAS Submit requires the respective store credentials to be configured in the Expo account.

Free Tier Limitations

The free tier covers individual development and small-scale projects. The limitations to be aware of:

LimitationFree tier
iOS cloud buildsLimited per month
Android cloud buildsHigher allowance than iOS
Update concurrencyLimited concurrent viewers
Team seatsOne developer
Build priorityStandard queue

Verify current limits on Expo's pricing page, as the free tier allowances change over time.

When a Paid Plan Is Needed

A paid plan is needed when:

  • Multiple developers need team access to EAS
  • Production-scale OTA updates require higher concurrency
  • Build volume exceeds the free tier monthly allowance
  • Priority build queues are needed for faster CI feedback

What Works Without an Account

All local development works with zero account. The complete command table, including --offline, --go, --localhost, and --tunnel flags, is documented in React Native Environment Setup.


Further Reading

Released under the MIT License.