# Expo Documentation
Expo is an open-source React Native framework for apps that run natively on Android, iOS, and the web. Expo brings together the best of mobile and the web and enables many important features for building and scaling an app such as live updates, instantly sharing your app, and web support. The company behind Expo also offers Expo Application Services (EAS), which are deeply integrated cloud services for Expo and React Native apps.
---
modificationDate: February 25, 2026
title: Introduction
description: Get started creating apps with Expo.
---
# Introduction
Get started creating apps with Expo.
Expo is a framework that makes developing Android and iOS apps easier. Our framework provides file-based routing, a standard library of native modules, and much more. Expo is open source with an active community on [GitHub](https://github.com/expo/expo) and [Discord](https://chat.expo.dev).
We also make [Expo Application Services (EAS)](https://expo.dev/eas), a set of services that complement the Expo framework in each step of the development process.
To get started visit:
[Quick start docs](/get-started/create-a-project) — Create a project, set up your development environment, and start developing.
---
---
modificationDate: February 25, 2026
title: Create a project
description: Learn how to create a new Expo project.
---
# Create a project
Learn how to create a new Expo project.
System requirements:
- [Node.js (LTS)](https://nodejs.org/en/).
- macOS, Windows (Powershell and [WSL 2](https://expo.fyi/wsl)), and Linux are supported.
We recommend starting with the default project created by `create-expo-app`. The default project includes example code to help you get started.
To create a new project, run the following command:
```sh
npx create-expo-app@latest
```
> You can choose a different template by adding the [`--template` option](/more/create-expo#--template).
## Next step
You have a project. Now it's time to set up your development environment so that you can start developing.
---
---
modificationDate: February 25, 2026
title: Set up your environment
description: Learn how to set up your development environment to start building with Expo.
---
# Set up your environment
Learn how to set up your development environment to start building with Expo.
Let's set up a local development environment for running your project on Android and iOS.
## Where would you like to develop?
We recommend using a real device to develop, since you'll get to see exactly what your users will see.
## How would you like to develop?
Expo Go is a playground for students and learners to try Expo quickly. A development build is a build of your own app that includes Expo's developer tools.
## Android device with Expo Go
### Set up an Android device with Expo Go
Scan the QR code to download the app from the Google Play Store, or visit the Expo Go page on the [Google Play Store](https://play.google.com/store/apps/details?id=host.exp.exponent&referrer=docs).
Download link: [https://play.google.com/store/apps/details?id=host.exp.exponent&referrer=docs](https://play.google.com/store/apps/details?id=host.exp.exponent&referrer=docs)
---
## Android device with a development build (EAS)
### Set up an Android device with a development build
#### Install EAS CLI
To build your app, you will need to install EAS CLI. You can do this by running the following command in your terminal:
```sh
npm install -g eas-cli
```
#### Create an Expo account and login
To build your app, you will need to create an Expo account and login to the EAS CLI.
1. [Sign up](https://expo.dev/signup) for an Expo account.
2. Run the following command in your terminal to log in to the EAS CLI:
```sh
eas login
```
#### Configure your project
Run the following command to create an EAS config in your project:
```sh
eas build:configure
```
#### Create a build
Run the following command to create a development build:
```sh
eas build --platform android --profile development
```
#### Install the development build on your device
After the build is complete, scan the QR code in your terminal or open the link on your device. Tap **Install** to download the build on your device, then tap **Open** to install it.
---
## Android device with a development build (local)
### Set up an Android device with a development build
### Install Watchman and JDK
##### macOS
##### Prerequisites
Use a package manager such as [Homebrew](https://brew.sh/) to install the following dependency.
##### Install dependencies
[Install Watchman](https://facebook.github.io/watchman/docs/install#macos) using a tool such as Homebrew:
```sh
brew install watchman
```
Install OpenJDK distribution called Azul Zulu using Homebrew. This distribution offers JDKs for both Apple Silicon and Intel Macs.
Run the following commands in a terminal:
```sh
brew install --cask zulu@17
```
After you install the JDK, add the `JAVA_HOME` environment variable in **~/.bash_profile** (or **~/.zshrc** if you use Zsh):
```bash
export JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home
```
##### Windows
##### Prerequisites
Use a package manager such as [Chocolatey](https://chocolatey.org/) to install the following dependencies.
##### Install dependencies
Install [Java SE Development Kit (JDK)](https://openjdk.org/):
```sh
choco install -y microsoft-openjdk17
```
##### Linux
##### Install dependencies
Follow [instructions from the Watchman documentation](https://facebook.github.io/watchman/docs/install#linux) to compile and install it from the source.
Install [Java SE Development Kit (JDK)](https://openjdk.org/):
You can download and install [OpenJDK@17](http://openjdk.java.net/) from [AdoptOpenJDK](https://adoptopenjdk.net/) or your system packager.
### Set up Android Studio
##### macOS
Download and install [Android Studio](https://developer.android.com/studio).
Open the **Android Studio** app, you will see the **SDK Components setup** screen. Click **Next** to continue to install the Android SDK and Android SDK Platform. Click **Next** again to verify the settings and install.
By default, Android Studio will install the latest version of the Android SDK. However, Android 15 (`VanillaIceCream`) SDK is required to compile a React Native app.
Open Android Studio, go to **Settings** > **Languages & Frameworks** > **Android SDK**. From the **SDK Platforms** tab, and under **Android 15 (`VanillaIceCream`)**, select **Android SDK Platform 35** and **Sources for Android 35**.
Then, click on the **SDK Tools** tab and make sure you have at least one version of the **Android SDK Build-Tools** and **Android Emulator** installed.
Copy or remember the path listed in the box that says **Android SDK Location**.
Add the following lines to your **/.zprofile** or **~/.zshrc** (if you are using bash, then **~/.bash_profile** or **~/.bashrc**) config file:
```sh
export ANDROID_HOME=$HOME/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/emulator
export PATH=$PATH:$ANDROID_HOME/platform-tools
```
Reload the path environment variables in your current shell:
```sh
source $HOME/.zshrc
source $HOME/.bashrc
```
Finally, make sure that you can run `adb` from your terminal.
**Troubleshooting: Android Studio not recognizing JDK**
If Android Studio doesn't recognize your homebrew installed JDK, you can create a Gradle configuration file to explicitly set the Java path:
1. Create a Gradle properties file in your home directory:
```sh
touch ~/.gradle/gradle.properties
```
2. Add the following line to the **gradle.properties** file, replacing the path with your actual Java installation path:
```bash gradle.properties
java.home=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home
```
3. If you have an existing `.gradle` folder in your project directory, delete it and reopen your project in Android Studio:
```sh
rm -rf .gradle
```
This should resolve issues with Android Studio not detecting your JDK installation.
##### Windows
Download [Android Studio](https://developer.android.com/studio).
Open **Android Studio Setup**. Under **Select components to install**, select Android Studio and Android Virtual Device. Then, click **Next**.
In the Android Studio Setup Wizard, under **Install Type**, select **Standard** and click **Next**.
The Android Studio Setup Wizard will ask you to verify the settings, such as the version of Android SDK, platform-tools, and so on. Click **Next** after you have verified.
In the next window, accept licenses for all available components.
By default, Android Studio will install the latest version of the Android SDK. However, Android 15 (`VanillaIceCream`) SDK is required to compile a React Native app.
Open Android Studio, go to **Settings** > **Languages & Frameworks** > **Android SDK**. From the **SDK Platforms** tab, and under **Android 15 (`VanillaIceCream`)**, select **Android SDK Platform 35** and **Sources for Android 35**.
Then, click on the **SDK Tools** tab and make sure you have at least one version of the **Android SDK Build-Tools** and **Android Emulator** installed.
After the tools installation is complete, configure the `ANDROID_HOME` environment variable. Go to **Windows Control Panel** > **User Accounts** > **User Accounts** (again) > **Change my environment variables** and click **New** to create a new `ANDROID_HOME` user variable. The value of this variable will point to the path to your Android SDK:
**How to find installed SDK location?**
By default, the Android SDK is installed at the following location:
```bash
%LOCALAPPDATA%\Android\Sdk
```
To find the location of the SDK in Android Studio manually, go to **Settings** > **Languages & Frameworks** > **Android SDK**. See the location next to **Android SDK Location**.
To verify that the new environment variable is loaded, open **PowerShell**, and copy and paste the following command:
```sh
Get-ChildItem -Path Env:
```
The command will output all user environment variables. In this list, see if `ANDROID_HOME` has been added.
To add platform-tools to the Path, go to **Windows Control Panel** > **User Accounts** > **User Accounts** (again) > **Change my environment variables** > **Path** > **Edit** > **New** and add the path to the platform-tools to the list as shown below:
**How to find installed platform-tools location**
By default, the platform-tools are installed at the following location:
```bash
%LOCALAPPDATA%\Android\Sdk\platform-tools
```
Finally, make sure that you can run `adb` from the PowerShell. For example, run the `adb --version` to see which version of the `adb` your system is running.
### Running your app on an Android device
#### Install expo-dev-client
Run the following command in your project's root directory:
```sh
npx expo install expo-dev-client
```
#### Enable debugging over USB
Most Android devices can only install and run apps downloaded from Google Play, by default. You will need to enable USB Debugging on your device to install your app during development.
To enable USB debugging on your device, you will first need to enable the "Developer options" menu by going to **Settings** > **About phone** > **Software information** and then tapping the `Build number` row at the bottom seven times. You can then go back to **Settings** > **Developer options** to enable "USB debugging".
#### Plug in your device via USB
Plug in your Android device via USB to your computer.
Check that your device is properly connecting to ADB, the Android Debug Bridge, by running `adb devices` in your terminal. You should see your device listed with `device` listed next to it. For example:
```sh
adb devices
List of devices attached
8AHX0T32K device
```
#### Run your app
Run the following from your terminal:
```sh
npx expo run:android
```
> This command runs a development server after building your app. You can skip running `npx expo start` on the next page.
---
## Android Emulator with Expo Go
### Set up an Android Emulator with Expo Go
### Set up Android Studio
##### macOS
Download and install [Android Studio](https://developer.android.com/studio).
Open the **Android Studio** app, you will see the **SDK Components setup** screen. Click **Next** to continue to install the Android SDK and Android SDK Platform. Click **Next** again to verify the settings and install.
By default, Android Studio will install the latest version of the Android SDK. However, Android 15 (`VanillaIceCream`) SDK is required to compile a React Native app.
Open Android Studio, go to **Settings** > **Languages & Frameworks** > **Android SDK**. From the **SDK Platforms** tab, and under **Android 15 (`VanillaIceCream`)**, select **Android SDK Platform 35** and **Sources for Android 35**.
Then, click on the **SDK Tools** tab and make sure you have at least one version of the **Android SDK Build-Tools** and **Android Emulator** installed.
Copy or remember the path listed in the box that says **Android SDK Location**.
Add the following lines to your **/.zprofile** or **~/.zshrc** (if you are using bash, then **~/.bash_profile** or **~/.bashrc**) config file:
```sh
export ANDROID_HOME=$HOME/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/emulator
export PATH=$PATH:$ANDROID_HOME/platform-tools
```
Reload the path environment variables in your current shell:
```sh
source $HOME/.zshrc
source $HOME/.bashrc
```
Finally, make sure that you can run `adb` from your terminal.
**Troubleshooting: Android Studio not recognizing JDK**
If Android Studio doesn't recognize your homebrew installed JDK, you can create a Gradle configuration file to explicitly set the Java path:
1. Create a Gradle properties file in your home directory:
```sh
touch ~/.gradle/gradle.properties
```
2. Add the following line to the **gradle.properties** file, replacing the path with your actual Java installation path:
```bash gradle.properties
java.home=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home
```
3. If you have an existing `.gradle` folder in your project directory, delete it and reopen your project in Android Studio:
```sh
rm -rf .gradle
```
This should resolve issues with Android Studio not detecting your JDK installation.
##### Windows
Download [Android Studio](https://developer.android.com/studio).
Open **Android Studio Setup**. Under **Select components to install**, select Android Studio and Android Virtual Device. Then, click **Next**.
In the Android Studio Setup Wizard, under **Install Type**, select **Standard** and click **Next**.
The Android Studio Setup Wizard will ask you to verify the settings, such as the version of Android SDK, platform-tools, and so on. Click **Next** after you have verified.
In the next window, accept licenses for all available components.
By default, Android Studio will install the latest version of the Android SDK. However, Android 15 (`VanillaIceCream`) SDK is required to compile a React Native app.
Open Android Studio, go to **Settings** > **Languages & Frameworks** > **Android SDK**. From the **SDK Platforms** tab, and under **Android 15 (`VanillaIceCream`)**, select **Android SDK Platform 35** and **Sources for Android 35**.
Then, click on the **SDK Tools** tab and make sure you have at least one version of the **Android SDK Build-Tools** and **Android Emulator** installed.
After the tools installation is complete, configure the `ANDROID_HOME` environment variable. Go to **Windows Control Panel** > **User Accounts** > **User Accounts** (again) > **Change my environment variables** and click **New** to create a new `ANDROID_HOME` user variable. The value of this variable will point to the path to your Android SDK:
**How to find installed SDK location?**
By default, the Android SDK is installed at the following location:
```bash
%LOCALAPPDATA%\Android\Sdk
```
To find the location of the SDK in Android Studio manually, go to **Settings** > **Languages & Frameworks** > **Android SDK**. See the location next to **Android SDK Location**.
To verify that the new environment variable is loaded, open **PowerShell**, and copy and paste the following command:
```sh
Get-ChildItem -Path Env:
```
The command will output all user environment variables. In this list, see if `ANDROID_HOME` has been added.
To add platform-tools to the Path, go to **Windows Control Panel** > **User Accounts** > **User Accounts** (again) > **Change my environment variables** > **Path** > **Edit** > **New** and add the path to the platform-tools to the list as shown below:
**How to find installed platform-tools location**
By default, the platform-tools are installed at the following location:
```bash
%LOCALAPPDATA%\Android\Sdk\platform-tools
```
Finally, make sure that you can run `adb` from the PowerShell. For example, run the `adb --version` to see which version of the `adb` your system is running.
### Set up an emulator
On the Android Studio main screen, click **More Actions**, then **Virtual Device Manager** in the dropdown.
Click the **Create device** button.
Under **Add device**, choose the type of hardware you'd like to emulate. We recommend testing against a variety of devices, but if you're unsure where to start, the newest device in the Pixel line could be a good choice.
Select an OS version to load on the emulator (probably one of the system images), and download the image (if required).
Change any other settings you'd like, and press **Finish** to create the emulator. You can now run this emulator anytime by pressing the Play button in the AVD Manager window.
### Install Expo Go
When you start a development server with `npx expo start` on the [start developing](/get-started/start-developing) page, press a to open the Android Emulator. Expo CLI will install Expo Go automatically.
---
## Android Emulator with a development build (EAS)
### Set up an Android Emulator with a development build
### Set up Android Studio
##### macOS
Download and install [Android Studio](https://developer.android.com/studio).
Open the **Android Studio** app, you will see the **SDK Components setup** screen. Click **Next** to continue to install the Android SDK and Android SDK Platform. Click **Next** again to verify the settings and install.
By default, Android Studio will install the latest version of the Android SDK. However, Android 15 (`VanillaIceCream`) SDK is required to compile a React Native app.
Open Android Studio, go to **Settings** > **Languages & Frameworks** > **Android SDK**. From the **SDK Platforms** tab, and under **Android 15 (`VanillaIceCream`)**, select **Android SDK Platform 35** and **Sources for Android 35**.
Then, click on the **SDK Tools** tab and make sure you have at least one version of the **Android SDK Build-Tools** and **Android Emulator** installed.
Copy or remember the path listed in the box that says **Android SDK Location**.
Add the following lines to your **/.zprofile** or **~/.zshrc** (if you are using bash, then **~/.bash_profile** or **~/.bashrc**) config file:
```sh
export ANDROID_HOME=$HOME/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/emulator
export PATH=$PATH:$ANDROID_HOME/platform-tools
```
Reload the path environment variables in your current shell:
```sh
source $HOME/.zshrc
source $HOME/.bashrc
```
Finally, make sure that you can run `adb` from your terminal.
**Troubleshooting: Android Studio not recognizing JDK**
If Android Studio doesn't recognize your homebrew installed JDK, you can create a Gradle configuration file to explicitly set the Java path:
1. Create a Gradle properties file in your home directory:
```sh
touch ~/.gradle/gradle.properties
```
2. Add the following line to the **gradle.properties** file, replacing the path with your actual Java installation path:
```bash gradle.properties
java.home=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home
```
3. If you have an existing `.gradle` folder in your project directory, delete it and reopen your project in Android Studio:
```sh
rm -rf .gradle
```
This should resolve issues with Android Studio not detecting your JDK installation.
##### Windows
Download [Android Studio](https://developer.android.com/studio).
Open **Android Studio Setup**. Under **Select components to install**, select Android Studio and Android Virtual Device. Then, click **Next**.
In the Android Studio Setup Wizard, under **Install Type**, select **Standard** and click **Next**.
The Android Studio Setup Wizard will ask you to verify the settings, such as the version of Android SDK, platform-tools, and so on. Click **Next** after you have verified.
In the next window, accept licenses for all available components.
By default, Android Studio will install the latest version of the Android SDK. However, Android 15 (`VanillaIceCream`) SDK is required to compile a React Native app.
Open Android Studio, go to **Settings** > **Languages & Frameworks** > **Android SDK**. From the **SDK Platforms** tab, and under **Android 15 (`VanillaIceCream`)**, select **Android SDK Platform 35** and **Sources for Android 35**.
Then, click on the **SDK Tools** tab and make sure you have at least one version of the **Android SDK Build-Tools** and **Android Emulator** installed.
After the tools installation is complete, configure the `ANDROID_HOME` environment variable. Go to **Windows Control Panel** > **User Accounts** > **User Accounts** (again) > **Change my environment variables** and click **New** to create a new `ANDROID_HOME` user variable. The value of this variable will point to the path to your Android SDK:
**How to find installed SDK location?**
By default, the Android SDK is installed at the following location:
```bash
%LOCALAPPDATA%\Android\Sdk
```
To find the location of the SDK in Android Studio manually, go to **Settings** > **Languages & Frameworks** > **Android SDK**. See the location next to **Android SDK Location**.
To verify that the new environment variable is loaded, open **PowerShell**, and copy and paste the following command:
```sh
Get-ChildItem -Path Env:
```
The command will output all user environment variables. In this list, see if `ANDROID_HOME` has been added.
To add platform-tools to the Path, go to **Windows Control Panel** > **User Accounts** > **User Accounts** (again) > **Change my environment variables** > **Path** > **Edit** > **New** and add the path to the platform-tools to the list as shown below:
**How to find installed platform-tools location**
By default, the platform-tools are installed at the following location:
```bash
%LOCALAPPDATA%\Android\Sdk\platform-tools
```
Finally, make sure that you can run `adb` from the PowerShell. For example, run the `adb --version` to see which version of the `adb` your system is running.
### Set up an emulator
On the Android Studio main screen, click **More Actions**, then **Virtual Device Manager** in the dropdown.
Click the **Create device** button.
Under **Add device**, choose the type of hardware you'd like to emulate. We recommend testing against a variety of devices, but if you're unsure where to start, the newest device in the Pixel line could be a good choice.
Select an OS version to load on the emulator (probably one of the system images), and download the image (if required).
Change any other settings you'd like, and press **Finish** to create the emulator. You can now run this emulator anytime by pressing the Play button in the AVD Manager window.
### Create a development build
#### Install EAS CLI
To build your app, you will need to install EAS CLI. You can do this by running the following command in your terminal:
```sh
npm install -g eas-cli
```
#### Create an Expo account and login
To build your app, you will need to create an Expo account and login to the EAS CLI.
1. [Sign up](https://expo.dev/signup) for an Expo account.
2. Run the following command in your terminal to log in to the EAS CLI:
```sh
eas login
```
#### Configure your project
Run the following command to create an EAS config in your project:
```sh
eas build:configure
```
#### Create a build
Run the following command to create a development build:
```sh
eas build --platform android --profile development
```
#### Install the development build on your emulator
After the build is complete, the CLI will prompt you to automatically download and install it on the Android Emulator. When prompted, press Y to directly install it on the emulator.
If you miss this prompt, you can download the build from the link provided in the terminal and drag and drop it onto the Android Emulator to install it.
---
## Android Emulator with a development build (local)
### Set up an Android Emulator with a development build
### Install Watchman and JDK
##### macOS
##### Prerequisites
Use a package manager such as [Homebrew](https://brew.sh/) to install the following dependency.
##### Install dependencies
[Install Watchman](https://facebook.github.io/watchman/docs/install#macos) using a tool such as Homebrew:
```sh
brew install watchman
```
Install OpenJDK distribution called Azul Zulu using Homebrew. This distribution offers JDKs for both Apple Silicon and Intel Macs.
Run the following commands in a terminal:
```sh
brew install --cask zulu@17
```
After you install the JDK, add the `JAVA_HOME` environment variable in **~/.bash_profile** (or **~/.zshrc** if you use Zsh):
```bash
export JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home
```
##### Windows
##### Prerequisites
Use a package manager such as [Chocolatey](https://chocolatey.org/) to install the following dependencies.
##### Install dependencies
Install [Java SE Development Kit (JDK)](https://openjdk.org/):
```sh
choco install -y microsoft-openjdk17
```
##### Linux
##### Install dependencies
Follow [instructions from the Watchman documentation](https://facebook.github.io/watchman/docs/install#linux) to compile and install it from the source.
Install [Java SE Development Kit (JDK)](https://openjdk.org/):
You can download and install [OpenJDK@17](http://openjdk.java.net/) from [AdoptOpenJDK](https://adoptopenjdk.net/) or your system packager.
### Set up Android Studio
##### macOS
Download and install [Android Studio](https://developer.android.com/studio).
Open the **Android Studio** app, you will see the **SDK Components setup** screen. Click **Next** to continue to install the Android SDK and Android SDK Platform. Click **Next** again to verify the settings and install.
By default, Android Studio will install the latest version of the Android SDK. However, Android 15 (`VanillaIceCream`) SDK is required to compile a React Native app.
Open Android Studio, go to **Settings** > **Languages & Frameworks** > **Android SDK**. From the **SDK Platforms** tab, and under **Android 15 (`VanillaIceCream`)**, select **Android SDK Platform 35** and **Sources for Android 35**.
Then, click on the **SDK Tools** tab and make sure you have at least one version of the **Android SDK Build-Tools** and **Android Emulator** installed.
Copy or remember the path listed in the box that says **Android SDK Location**.
Add the following lines to your **/.zprofile** or **~/.zshrc** (if you are using bash, then **~/.bash_profile** or **~/.bashrc**) config file:
```sh
export ANDROID_HOME=$HOME/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/emulator
export PATH=$PATH:$ANDROID_HOME/platform-tools
```
Reload the path environment variables in your current shell:
```sh
source $HOME/.zshrc
source $HOME/.bashrc
```
Finally, make sure that you can run `adb` from your terminal.
**Troubleshooting: Android Studio not recognizing JDK**
If Android Studio doesn't recognize your homebrew installed JDK, you can create a Gradle configuration file to explicitly set the Java path:
1. Create a Gradle properties file in your home directory:
```sh
touch ~/.gradle/gradle.properties
```
2. Add the following line to the **gradle.properties** file, replacing the path with your actual Java installation path:
```bash gradle.properties
java.home=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home
```
3. If you have an existing `.gradle` folder in your project directory, delete it and reopen your project in Android Studio:
```sh
rm -rf .gradle
```
This should resolve issues with Android Studio not detecting your JDK installation.
##### Windows
Download [Android Studio](https://developer.android.com/studio).
Open **Android Studio Setup**. Under **Select components to install**, select Android Studio and Android Virtual Device. Then, click **Next**.
In the Android Studio Setup Wizard, under **Install Type**, select **Standard** and click **Next**.
The Android Studio Setup Wizard will ask you to verify the settings, such as the version of Android SDK, platform-tools, and so on. Click **Next** after you have verified.
In the next window, accept licenses for all available components.
By default, Android Studio will install the latest version of the Android SDK. However, Android 15 (`VanillaIceCream`) SDK is required to compile a React Native app.
Open Android Studio, go to **Settings** > **Languages & Frameworks** > **Android SDK**. From the **SDK Platforms** tab, and under **Android 15 (`VanillaIceCream`)**, select **Android SDK Platform 35** and **Sources for Android 35**.
Then, click on the **SDK Tools** tab and make sure you have at least one version of the **Android SDK Build-Tools** and **Android Emulator** installed.
After the tools installation is complete, configure the `ANDROID_HOME` environment variable. Go to **Windows Control Panel** > **User Accounts** > **User Accounts** (again) > **Change my environment variables** and click **New** to create a new `ANDROID_HOME` user variable. The value of this variable will point to the path to your Android SDK:
**How to find installed SDK location?**
By default, the Android SDK is installed at the following location:
```bash
%LOCALAPPDATA%\Android\Sdk
```
To find the location of the SDK in Android Studio manually, go to **Settings** > **Languages & Frameworks** > **Android SDK**. See the location next to **Android SDK Location**.
To verify that the new environment variable is loaded, open **PowerShell**, and copy and paste the following command:
```sh
Get-ChildItem -Path Env:
```
The command will output all user environment variables. In this list, see if `ANDROID_HOME` has been added.
To add platform-tools to the Path, go to **Windows Control Panel** > **User Accounts** > **User Accounts** (again) > **Change my environment variables** > **Path** > **Edit** > **New** and add the path to the platform-tools to the list as shown below:
**How to find installed platform-tools location**
By default, the platform-tools are installed at the following location:
```bash
%LOCALAPPDATA%\Android\Sdk\platform-tools
```
Finally, make sure that you can run `adb` from the PowerShell. For example, run the `adb --version` to see which version of the `adb` your system is running.
### Set up an emulator
On the Android Studio main screen, click **More Actions**, then **Virtual Device Manager** in the dropdown.
Click the **Create device** button.
Under **Add device**, choose the type of hardware you'd like to emulate. We recommend testing against a variety of devices, but if you're unsure where to start, the newest device in the Pixel line could be a good choice.
Select an OS version to load on the emulator (probably one of the system images), and download the image (if required).
Change any other settings you'd like, and press **Finish** to create the emulator. You can now run this emulator anytime by pressing the Play button in the AVD Manager window.
### Running your app on an Android Emulator
#### Install expo-dev-client
Run the following command in your project's root directory:
```sh
npx expo install expo-dev-client
```
Run the following from your terminal:
```sh
npx expo run:android
```
> This command runs a development server after building your app. You can skip running `npx expo start` on the next page.
---
## iOS device with Expo Go
### Set up an iOS device with Expo Go
Scan the QR code to download the app from the App Store, or visit the Expo Go page on the [App Store](https://itunes.apple.com/app/apple-store/id982107779).
Download link: [https://itunes.apple.com/app/apple-store/id982107779](https://itunes.apple.com/app/apple-store/id982107779)
---
## iOS device with a development build (EAS)
### Set up an iOS device with a development build
#### Enroll in the Apple Developer Program
To install a development build on your iOS device, you will need an active subscription to the Apple Developer Program. Sign up for the [Apple Developer Program here](https://developer.apple.com/programs/).
#### Install EAS CLI
To build your app, you will need to install EAS CLI. You can do this by running the following command in your terminal:
```sh
npm install -g eas-cli
```
#### Create an Expo account and login
Next, you will need to create an Expo account and login to the EAS CLI.
1. [Sign up](https://expo.dev/signup) for an Expo account.
2. Run the following command in your terminal to log in to the EAS CLI:
```sh
eas login
```
#### Configure your project
Run the following command to create an EAS config in your project:
```sh
eas build:configure
```
#### Create an ad hoc provisioning profile
To install a development build on your iOS device, you will need to create an ad hoc provisioning profile. Create one by running the following command in your terminal:
```sh
eas device:create
```
#### Create a development build
Run the following command to create a development build:
```sh
eas build --platform ios --profile development
```
#### Install the development build on your device
After the build is complete, scan the QR code in your terminal and tap **Open with iTunes** when it appears inside the Camera app. Alternatively, open the link displayed in the terminal on your device.
After confirming the installation, the app will appear in your device's app library.
#### Turn on developer mode
1. Open **Settings** > **Privacy & Security**, scroll down to the **Developer Mode** list item and navigate into it.
2. Tap the switch to enable **Developer Mode**. After you do so, Settings presents an alert to warn you that Developer Mode reduces your device's security. To continue enabling **Developer Mode**, tap the alert's **Restart** button.
3. After the device restarts and you unlock it, the device shows an alert confirming that you want to enable Developer Mode. Tap **Turn On**, and enter your device passcode when prompted.
> Alternatively, if you have Xcode installed on your Mac, you can use it to [enable iOS developer mode](/guides/ios-developer-mode/#connect-an-ios-device-with-a-mac).
---
## iOS device with a development build (local)
### Set up an iOS device with a development build
### Set up Xcode and Watchman
#### Install Xcode
Open up the Mac App Store, search for [Xcode](https://apps.apple.com/us/app/xcode/id497799835), and click **Install** (or **Update** if you have it already).
#### Install Xcode Command Line Tools
Open Xcode, choose **Settings...** from the Xcode menu (or press cmd ⌘ + ,). Go to the **Locations** and install the tools by selecting the most recent version in the **Command Line Tools** dropdown.
#### Install an iOS Simulator in Xcode
To install an iOS Simulator, open **Xcode > Settings... > Components**, and under **Platform Support > iOS ...**, click **Get**.
#### Install Watchman
[Watchman](https://facebook.github.io/watchman/docs/install#macos) is a tool for watching changes in the filesystem. Installing it will result in better performance. You can install it with:
```sh
brew update
brew install watchman
```
### Configure your project
#### Install expo-dev-client
Run the following command in your project's root directory:
```sh
npx expo install expo-dev-client
```
#### Plug in your device via USB and enable developer mode
1. Connect your iOS device to your Mac using a USB cable. Unlock the device and tap **Trust** if prompted.
2. Open Xcode. From the menu bar, select **Window** > **Devices and Simulators**. You will see a warning in Xcode to enable developer mode.
3. On your iOS device, open **Settings** > **Privacy & Security**, scroll down to the **Developer Mode** list item and navigate into it.
4. Tap the switch to enable **Developer Mode**. After you do so, Settings presents an alert to warn you that Developer Mode reduces your device's security. To continue enabling **Developer Mode**, tap the alert's **Restart** button.
5. After the device restarts and you unlock it, the device shows an alert confirming that you want to enable Developer Mode. Tap **Turn On**, and enter your device passcode when prompted.
#### Run the project on your device
1. Add the `ios.bundleIdentifier` in the **app.json** file in the root directory to a unique value so that Xcode generates the provisioning profile for the app signing step.
2. Run the following command in your project's root directory and select your plugged in device from the list:
```sh
npx expo run:ios --device
```
> This command runs a development server after building your app. You can skip running `npx expo start` on the next page.
---
## iOS Simulator with Expo Go
### Set up an iOS Simulator with Expo Go
### Set up Xcode
#### Install Xcode
Open up the Mac App Store, search for [Xcode](https://apps.apple.com/us/app/xcode/id497799835), and click **Install** (or **Update** if you have it already).
#### Install Xcode Command Line Tools
Open Xcode, choose **Settings...** from the Xcode menu (or press cmd ⌘ + ,). Go to the **Locations** and install the tools by selecting the most recent version in the **Command Line Tools** dropdown.
#### Install an iOS Simulator in Xcode
To install an iOS Simulator, open **Xcode > Settings... > Components**, and under **Platform Support > iOS ...**, click **Get**.
#### Install Watchman
[Watchman](https://facebook.github.io/watchman/docs/install#macos) is a tool for watching changes in the filesystem. Installing it will result in better performance. You can install it with:
```sh
brew update
brew install watchman
```
### Install Expo Go
When you start a development server with `npx expo start` on the [start developing](/get-started/start-developing) page, press i to open the iOS Simulator. Expo CLI will install Expo Go automatically.
---
## iOS Simulator with a development build (EAS)
### Set up an iOS Simulator with a development build
### Set up Xcode
#### Install Xcode
Open up the Mac App Store, search for [Xcode](https://apps.apple.com/us/app/xcode/id497799835), and click **Install** (or **Update** if you have it already).
#### Install Xcode Command Line Tools
Open Xcode, choose **Settings...** from the Xcode menu (or press cmd ⌘ + ,). Go to the **Locations** and install the tools by selecting the most recent version in the **Command Line Tools** dropdown.
#### Install an iOS Simulator in Xcode
To install an iOS Simulator, open **Xcode > Settings... > Components**, and under **Platform Support > iOS ...**, click **Get**.
#### Install Watchman
[Watchman](https://facebook.github.io/watchman/docs/install#macos) is a tool for watching changes in the filesystem. Installing it will result in better performance. You can install it with:
```sh
brew update
brew install watchman
```
### Create a development build
#### Install EAS CLI
To build your app, you will need to install EAS CLI. You can do this by running the following command in your terminal:
```sh
npm install -g eas-cli
```
#### Create an Expo account and login
Next, you will need to create an Expo account and login to the EAS CLI.
1. [Sign up](https://expo.dev/signup) for an Expo account.
2. Run the following command in your terminal to log in to the EAS CLI:
```sh
eas login
```
#### Configure your project
Run the following command to create an EAS config in your project:
```sh
eas build:configure
```
#### Adjust your build profile
To create a simulator-compatible development build, you'll need to update your build profile in **eas.json** to set the `ios.simulator` property to `true`:
```json eas.json
{
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
/* @info */
"ios": {
"simulator": true
}
/* @end */
}
}
}
```
#### Create a development build
Run the following command to create a development build:
```sh
eas build --platform ios --profile development
```
#### Install the development build on your simulator
After the build is complete, the CLI will prompt you to automatically download and install it on the iOS Simulator. When prompted, press Y to directly install it on the simulator.
If you miss this prompt, you can download the build from the link provided in the terminal and drag and drop it onto the iOS Simulator to install it.
---
## iOS Simulator with a development build (local)
### Set up an iOS Simulator with a development build
### Set up Xcode and Watchman
#### Install Xcode
Open up the Mac App Store, search for [Xcode](https://apps.apple.com/us/app/xcode/id497799835), and click **Install** (or **Update** if you have it already).
#### Install Xcode Command Line Tools
Open Xcode, choose **Settings...** from the Xcode menu (or press cmd ⌘ + ,). Go to the **Locations** and install the tools by selecting the most recent version in the **Command Line Tools** dropdown.
#### Install an iOS Simulator in Xcode
To install an iOS Simulator, open **Xcode > Settings... > Components**, and under **Platform Support > iOS ...**, click **Get**.
#### Install Watchman
[Watchman](https://facebook.github.io/watchman/docs/install#macos) is a tool for watching changes in the filesystem. Installing it will result in better performance. You can install it with:
```sh
brew update
brew install watchman
```
### Running your app on an iOS Simulator
#### Install expo-dev-client
Run the following command in your project's root directory:
```sh
npx expo install expo-dev-client
```
Run the following from your terminal:
```sh
npx expo run:ios
```
> This command runs a development server after building your app. You can skip running `npx expo start` on the next page.
## Next step
You have a project and a development environment. Now it's time to start developing.
---
---
modificationDate: February 25, 2026
title: Start developing
description: Make your first change to an Expo project and see it live on your device.
---
# Start developing
Make your first change to an Expo project and see it live on your device.
## Start a development server
To start the development server, run the following command:
```sh
npx expo start
```
## Open the app on your device
After running the command above, you will see a QR code in your terminal. Scan this QR code to open the app on your device.
If you're using an Android Emulator or iOS Simulator, you can press a or i respectively to open the app.
Having problems?
Make sure you are on the same Wi-Fi network on your computer and your device.
If it still doesn't work, it may be due to the router configuration — this is common for public networks. You can work around this by choosing the **Tunnel** connection type when starting the development server, then scanning the QR code again.
```sh
npx expo start --tunnel
```
> Using the **Tunnel** connection type will make the app reloads considerably slower than on **LAN** or **Local**, so it's best to avoid tunnel when possible. You may want to install and use an emulator or simulator to speed up development if **Tunnel** is required to access your machine from another device on your network.
## Make your first change
Open the **app/(tabs)/index.tsx** file in your code editor and make a change.
```diff
}
>
- Welcome!
+ Hello World!
```
Changes not showing up on your device?
Expo Go is configured by default to automatically reload the app whenever a file is changed, but let's make sure to go over the steps to enable it in case somehow things aren't working.
- Make sure you have the [development mode enabled in Expo CLI](/workflow/development-mode#development-mode).
- Close the Expo app and reopen it.
- Once the app is open again, shake your device to reveal the developer menu. If you are using an emulator, press Ctrl + M for Android or Cmd ⌘ + D for iOS.
- If you see **Enable Fast Refresh**, press it. If you see **Disable Fast Refresh**, dismiss the developer menu. Now try making another change.
## File structure
Below, you can get familiar with the default project's file structure:
Files
### app
Contains the app's navigation, which is file-based. The file structure of the **app** directory determines the app's navigation.
The app has two routes defined by two files: **app/(tabs)/index.tsx** and **app/(tabs)/explore.tsx**. The layout file in **app/(tabs)/_layout.tsx** sets up the tab navigator.
## Features
The default project template has the following features:
Default project
### File-based routing
The app has two screens: **app/(tabs)/index.tsx** and **app/(tabs)/explore.tsx**. The layout file in **app/(tabs)/_layout.tsx** sets up the tab navigator.
---
---
modificationDate: February 25, 2026
title: Next steps
description: Develop, review, and submit your project.
---
# Next steps
Develop, review, and submit your project.
Here are next steps to continue building your app:
### Reset your project
You can remove the boilerplate code and start fresh with a new project. Run the following command to reset your project:
```sh
npm run reset-project
```
This command will move the existing files in **app** to **app-example**, then create a new **app** directory with a new **index.tsx** file.
### Develop, review, and deploy
Learn how to develop by reading the docs in the Develop section. You'll learn how to create [UI elements](/develop/user-interface/splash-screen-and-app-icon), add [unit tests](/develop/unit-testing), include [native modules](/config-plugins/introduction), and more.
Once you've developed your app, you can share it with your teammates for [review](/review/overview).
Finally, you can [build](/deploy/build-project) and [submit](/deploy/submit-to-app-stores) your project to the app stores.
### Step-by-step guide
For a guided, step-by-step walkthrough of building an app with Expo from start to finish, check out the [tutorial](/tutorial/introduction).
---
---
modificationDate: February 25, 2026
title: Tools for development
description: An overview of Expo tools and websites that will help you during various aspects of your project-building journey.
---
# Tools for development
An overview of Expo tools and websites that will help you during various aspects of your project-building journey.
When you create a new project with Expo, learning about the following essential tools and websites can help you during your app development journey. This page provides an overview of a list of recommended tools.
## Expo CLI
Expo CLI is a development tool and is installed automatically with the `expo` package when you create a new project. You can use it by leveraging `npx` (a Node.js package runner).
It is designed to help you move faster during the app development phase. For example, your first interaction with Expo CLI is starting the development server by running the command: `npx expo start`.
The following is a list of common commands that you will use with Expo CLI while developing your app:
| Command | Description |
| --- | --- |
| `npx expo start` | Starts the development server (whether you are using a development build or Expo Go). |
| `npx expo prebuild` | Generates native Android and iOS directories using [Prebuild](/workflow/prebuild). |
| `npx expo run:android` | Compiles native Android app locally. |
| `npx expo run:ios` | Compiles native iOS app locally. |
| `npx expo install package-name` | Used to install a new library or validate and update specific libraries in your project by adding `--fix` option to this command. |
| `npx expo lint` | [Setup and configures](/guides/using-eslint) ESLint. If ESLint is already configured, this command will [lint your project files](/guides/using-eslint#usage). |
In a nutshell, Expo CLI allows you to develop, compile, start your app, and more. See [Expo CLI reference](/more/expo-cli) for more available options and actions you can perform with the CLI.
## EAS CLI
EAS CLI is used to log in to your Expo account and compile your app using different EAS services such as Build, Update, or Submit. You can also use this tool to:
- Publish your app to the app stores
- Create a development, preview, or production build of your app
- Create over-the-air (OTA) updates
- Manage your app credentials
- Create an ad hoc provisioning profile for an iOS device
To use EAS CLI, you need to install it globally on your local machine by running the command:
```sh
npm install -g eas-cli
```
You can use `eas --help` in your terminal window to learn more about the available commands. For a complete reference, see [`eas-cli` npm page](https://www.npmjs.com/package/eas-cli).
## Expo Doctor
Expo Doctor is a command line tool used to diagnose issues in your Expo project. To use it, run the following command in your project's root directory:
```sh
npx expo-doctor
```
This command performs checks and analyzes your project's codebase for common issues in [app config](/workflow/configuration) and **package.json** files, dependency compatibility, configuration files, and the overall health of the project. Once the check is complete, Expo Doctor outputs the results.
If Expo Doctor finds an issue, it provides a description of the problem along with advice on how to fix it or where to find help.
By default, Expo Doctor validates your project's packages against the [React Native directory](https://reactnative.directory/) and checks if app config properties are properly synced when native directories exist. You can configure these checks in your project's **package.json** file. See [`reactNativeDirectoryCheck`](/versions/latest/config/package-json#reactnativedirectorycheck) and [`appConfigFieldsNotSyncedCheck`](/versions/latest/config/package-json#appconfigfieldsnotsynced) for more details.
You can also use `npx expo-doctor --help` to display usage information.
## Orbit
Orbit is a macOS and Windows app that enables:
- Install and launch builds from EAS on physical devices and emulators.
- Install and launch updates from EAS on Android Emulators or iOS Simulators.
- Launch snack projects on Android Emulators or iOS Simulators.
- Use local files to install and launch apps. Orbit supports any Android **.apk**, iOS Simulator compatible **.app**, or ad hoc signed apps.
- See a list of pinned projects from your EAS dashboard.
### Installation
You can download Orbit with Homebrew for macOS, or directly from the [GitHub releases](https://github.com/expo/orbit/releases).
```sh
brew install expo-orbit
```
If you want Orbit to start when you log in automatically, click on the Orbit icon in the menu bar, then **Settings** and select the **Launch on Login** option.
> Orbit relies on the Android SDK on both macOS and Windows and `xcrun` for device management only on macOS, which requires setting up both [Android Studio](/workflow/android-studio-emulator) and [Xcode](/workflow/ios-simulator).
## Expo Tools for VS Code
Expo Tools is a VS Code extension to improve your development experience when working with app config files. It provides features such as autocomplete and intellisense for files such as app config, EAS config, store config and Expo Module config files.
[Install Expo Tools VS Code extension](https://marketplace.visualstudio.com/items?itemName=expo.vscode-expo-tools) — Use this link to install the extension or search Expo Tools directly in your VS Code editor.
You can also use it to debug your app using VS Code's built-in debugger to set breakpoints, inspect variables, execute code through the debug console, and more. See [Debugging with VS Code](/debugging/tools#debugging-with-vs-code) for how to use this extension for debugging.
## Test prototypes with Snack and Expo Go
### Snack
Snack is an in-browser development environment that works similarly to Expo Go. It's a great way to share code snippets and experiment with React Native without downloading any tools on your computer.
To use it, go to [snack.expo.dev](https://snack.expo.dev/), edit the `` component in **App.js**, choose a platform (Android, iOS, or web) in the right panel and see the changes live.
### Expo Go
[Expo Go](https://expo.dev/go) is a free, open-source playground for students and learners to try out React Native. It works with Android and iOS.
For more information on how to use it:
- Click [this link](/get-started/set-up-your-environment?mode=expo-go) to go to Set up your environment guide
- Select a platform to develop under **Where would you like to develop?**
- Select Expo Go under **How would you like to develop?**
- Follow the instructions described in that guide
> **Note:** Expo Go is limited and not useful for building production-grade projects. Use [development builds](/get-started/set-up-your-environment?mode=development-build) instead.
What if I open a project with an unsupported SDK version?
When running a project that was created for an unsupported SDK version in Expo Go, you'll see the following error:
```sh
"Project is incompatible with this version of Expo Go"
```
To fix this, upgrading your project to a [supported SDK version](/versions/latest#each-expo-sdk-version-depends-on-a-react-native-version) is recommended. If you want to learn how to do it, see [Upgrade the project to a new SDK Version](/develop/tools#how-do-i-upgrade-my-project-from).
How do I upgrade my project from an unsupported SDK version?
See [Upgrading Expo SDK guide](/workflow/upgrading-expo-sdk-walkthrough) for instructions for upgrading to a specific SDK version.
## React Native directory
Any library that is compatible with React Native works in an Expo project when you use a development build to create your project.
[reactnative.directory](https://reactnative.directory/) is a searchable database for React Native libraries. If a library you are looking for is not included in Expo SDK, use the directory to find a compatible library for your project.
[Use libraries](/workflow/using-libraries) — See this guide to learn more about the difference between React Native core libraries, Expo SDK libraries, and third-party libraries. It also explains how to determine third-party library compatibility.
---
---
modificationDate: February 25, 2026
title: Navigation in Expo and React Native apps
description: Learn about the recommended approach for integrating navigation in an Expo and React Native project.
---
# Navigation in Expo and React Native apps
Learn about the recommended approach for integrating navigation in an Expo and React Native project.
The core React Native library does not include a built-in navigation solution, so you can choose a navigation library that best fits your needs. For Expo and React Native apps, it is generally a choice between [React Navigation](https://reactnavigation.org/) or [Expo Router](/router/introduction).
## Why React Native apps needs a navigation library
React Native core includes basic UI components, touch handling, device APIs and networking, but excludes, among other things, storage, camera, maps, most device sensors, and **navigation**! These are intended to be covered by community libraries.
## React Navigation
React Navigation is a component-based navigation library widely used across the React Native ecosystem. It lets you compose stack, tab, and drawer navigators entirely in code so you can implement complex flows, custom transitions, and app-specific UX patterns.
The library offers platform-specific look-and-feel with smooth animations and gestures, unified mobile and web routing, automatic deep links, type routes with static configuration, and is highly customizable.
[React Navigation: Getting started](https://reactnavigation.org/docs/getting-started) — Learn how to get started with React Navigation.
## Expo Router (recommended for Expo projects)
Expo Router is a file-based routing library for Expo and React Native projects and is a built on top of React Navigation. By following the **app** directory convention, it turns files into routes and is integrated with Expo for [Expo CLI](/more/expo-cli) and bundling without additional setup. The library also adds features such as typed routes, dynamic routes, lazy bundling in development, static rendering for the web, and automatic deep linking.
New Expo projects created with `npx create-expo-app@latest` include Expo Router by default so you can ship cross-platform navigation quickly while still being able to reach for React Navigation APIs when needed.
[Introduction to Expo Router](/router/introduction) — Expo Router is an open-source routing library for Universal React Native applications built with Expo.
[Installation](/router/installation) — Learn how to quickly get started by creating a new project with Expo Router or adding the library to an existing project.
[Core concepts](/router/basics/core-concepts) — Learn about the core concepts of file-based routing in Expo.
---
---
modificationDate: February 25, 2026
title: Splash screen and app icon
description: Learn how to add a splash screen and app icon to your Expo project.
---
# Splash screen and app icon
Learn how to add a splash screen and app icon to your Expo project.
A splash screen and an app icon are fundamental elements of a mobile app. They play an important role in the user experience and branding of the app. This guide provides steps on how to create and add them to your app.
[Create an App Icon and Splash Screen](https://www.youtube.com/watch?v=3Bsw8a1BJoQ) — See a detailed walkthrough on how to create an app icon and splash screen for an Expo project.
## Splash screen
A splash screen, also known as a launch screen, is the first screen a user sees when they open your app. It stays visible while the app is loading. You can also control the behavior of when a splash screen disappears by using the native [SplashScreen API](/versions/latest/sdk/splash-screen).
The [`expo-splash-screen`](/versions/latest/sdk/splash-screen) has a built-in [config plugin](/config-plugins/introduction) that lets you configure properties such as the splash icon and background color.
> **Do not use Expo Go or a development build to test your splash screen**. Expo Go renders your app icon while the splash screen is visible, which can interfere with testing. Development builds include `expo-dev-client`, which has its own splash screen and may cause conflicts. **Instead, use a [preview build](/build/eas-json#preview-builds) or a [production build](/build/eas-json#production-builds)**.
### Create a splash screen icon
To create a splash screen icon, you can use this [Figma template](https://www.figma.com/community/file/1466490409418563617). It provides a bare minimum design for an icon and splash images for Android and iOS.
**Recommended:**
- Use a 1024x1024 image.
- Use a **.png** file.
- Use a transparent background.
### Export the splash icon as a .png
After creating a splash screen icon, export it as a **.png** and save it in the **assets/images** directory. By default, Expo uses **splash-icon.png** as the file name. If you decide to change the name of your splash screen file, make sure to use that in the next step.
> **Note:** **Currently, only .png images are supported** to use as a splash screen icon in an Expo project. If you use another image format, making a production build of your app will fail.
### Configure the splash screen icon
Open the app config file, and under plugins, set the following properties:
```json
{
"expo": {
"plugins": [
[
"expo-splash-screen",
{
"backgroundColor": "#232323",
"image": "./assets/images/splash-icon.png",
"dark": {
"image": "./assets/images/splash-icon-dark.png",
"backgroundColor": "#000000"
},
"imageWidth": 200
}
]
]
}
}
```
To test your new splash screen, build your app for [internal distribution](/tutorial/eas/internal-distribution-builds) or for production, see guides on [Android](/tutorial/eas/android-production-build) and [iOS](/tutorial/eas/ios-production-build).
[Configurable splash screen properties](/versions/latest/sdk/splash-screen#configurable-properties) — Learn about the configurable properties of the SplashScreen API.
Configuring `expo-splash-screen` properties separately for Android and iOS
[`expo-splash-screen`](/versions/latest/sdk/splash-screen) also supports `android` and `ios` properties for configuring the splash screen for a specific platform. See the following example:
```json
{
"expo": {
"plugins": [
[
"expo-splash-screen",
{
"ios": {
"backgroundColor": "#ffffff",
"image": "./assets/images/splash-icon.png",
"resizeMode": "cover"
},
"android": {
"backgroundColor": "#0c7cff",
"image": "./assets/images/splash-android-icon.png",
"imageWidth": 150
}
}
]
]
}
}
```
Not using prebuild?
If your app does not use [Expo Prebuild](/workflow/prebuild) (formerly the _managed workflow_) to generate the native **android** and **ios** directories, then changes in the app config will have no effect. For more information, see [how you can customize the configuration manually](https://github.com/expo/expo/tree/main/packages/expo-splash-screen#-installation-in-bare-react-native-projects).
Troubleshooting: New splash screen not appearing on iOS
For SDK versions below 52, in iOS development builds, launch screens can sometimes remain cached between builds, making it harder to test new images. Apple recommends clearing the _derived data_ directory before rebuilding, this can be done with Expo CLI by running:
```sh
npx expo run:ios --no-build-cache
```
See [Apple's guide on testing launch screens](https://developer.apple.com/documentation/technotes/tn3118-debugging-your-apps-launch-screen) for more information.
## App icon
An app's icon is what your app users see on their device's home screen and app stores. Android and iOS have different and strict requirements.
### Create an app icon
To create an app icon, you can use this [Figma template](https://www.figma.com/community/file/1466490409418563617). It provides a bare minimum design for an icon and splash images for Android and iOS.
### Export the icon image as a .png
After creating an app icon, export it as **.png** and save it in the **assets/images** directory. By default, Expo uses **icon.png** as the file name. If you decide to use a different file name, make sure to use that in the next step.
### Add the icon in app config
Open the app config and add the local path as the value of [`icon`](/versions/latest/config/app#icon) property to point it to your new app icon:
```json
{
"icon": "./assets/images/icon.png"
}
```
Custom configuration tips for Android and iOS
#### Android
Further customization of the Android icon is possible using the [`android.adaptiveIcon`](/versions/latest/config/app#adaptiveicon) property, which will override both of the previously mentioned settings.
The Android Adaptive Icon is formed from two separate layers — a foreground image and a background color or image. This allows the OS to mask the icon into different shapes and also supports visual effects. For Android 13 and later, the OS supports a themed app icon that uses a wallpaper and theme to determine the color set by the device's theme.
The design you provide should follow the [Android Adaptive Icon Guidelines](https://developer.android.com/develop/ui/views/launch/icon_design_adaptive) for launcher icons. You should also:
- Use **.png** files.
- Use the `android.adaptiveIcon.foregroundImage` property to specify the path to your foreground image.
- Use the `android.adaptiveIcon.monochromeImage` property to specify the path to your monochrome image.
- The default background color is white; to specify a different background color, use the `android.adaptiveIcon.backgroundColor` property. You can instead specify a background image using the `android.adaptiveIcon.backgroundImage` property. Make sure that it has the same dimensions as your foreground image.
You may also want to provide a separate icon for older Android devices that do not support Adaptive Icons. You can do so with the `android.icon` property. This single icon would be a combination of your foreground and background layers.
> See [Apple best practices](https://developer.apple.com/design/human-interface-guidelines/app-icons/#Best-practices) to ensure your icon looks professional, such as testing your icon on different wallpapers and avoiding text beside your product's wordmark. Provide an icon that's at least 512x512 pixels.
#### iOS
[Icon Composer](https://www.youtube.com/watch?v=RZ_QMym3adw) — Learn how to use the new Icon Composer to create app icons for an Expo project.
For iOS, your app's icon should follow the [Apple Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/app-icons/). You can use the [Icon Composer](https://developer.apple.com/icon-composer/) app to create your app icon. This will output a **.icon** directory that you can add to your project's **assets** directory. You can then provide the path to this directory in your app config. Adding support for dark mode is handled in Icon Composer, so you do not need to provide variants when using this approach.
> **Note:** Providing an Icon Composer **.icon** directory via `ios.icon` is supported **in SDK 54** and later.
```json
{
"expo": {
"ios": {
"icon": "./assets/app.icon"
}
}
}
```
Alternatively, the previous approach of providing an image is still supported. You should:
- Use a **.png** file.
- 1024x1024 is a good size. If you have an Expo project created using `npx create-expo-app`, [EAS Build](/build/setup) will generate the other sizes for you. In case of a bare React Native project, generate the icons on your own. The largest size EAS Build generates is 1024x1024.
- The icon must be exactly square. For example, a 1023x1024 icon is not valid.
- Make sure the icon fills the whole square, with no rounded corners or other transparent pixels. The operating system will mask your icon when appropriate.
- Use `ios.icon` to specify different icons for various system appearances (for example, dark and tinted) can be provided. If specified, this overrides the top-level icon key in the app config file. See the example below:
```json
{
"expo": {
"ios": {
"icon": {
"dark": "./assets/images/ios-dark.png",
"light": "./assets/images/ios-light.png",
"tinted": "./assets/images/ios-tinted.png"
}
}
}
}
```
---
---
modificationDate: February 25, 2026
title: Safe areas
description: Learn how to add safe areas for screen components inside your Expo project.
---
# Safe areas
Learn how to add safe areas for screen components inside your Expo project.
Creating a safe area ensures your app screen's content is positioned correctly. This means it doesn't get overlapped by notches, status bars, home indicators, and other interface elements that are part of the device's physical hardware or are controlled by the operating system. When the content gets overlapped, it gets concealed by these interface elements.
Here's an example of an app screen's content getting concealed by the status bar on Android. On iOS, the same content is concealed by rounded corners, notch, and the status bar.
## Use `react-native-safe-area-context` library
[`react-native-safe-area-context`](https://github.com/AppAndFlow/react-native-safe-area-context) provides a flexible API for handling Android and iOS device's safe area insets. It also provides a `SafeAreaView` component that you can use instead of a [``](https://reactnative.dev/docs/view) to account for safe areas automatically in your screen components.
Using the library, the result of the previous example changes as it displays the content inside a safe area, as shown below:
### Installation
You can skip installing `react-native-safe-area-context` if you have created a project using [the default template](/get-started/create-a-project). This library is installed as peer dependency for Expo Router library. Otherwise, install it by running the following command:
```sh
npx expo install react-native-safe-area-context
```
### Usage
You can directly use [`SafeAreaView`](https://appandflow.github.io/react-native-safe-area-context/api/safe-area-view) to wrap the content of your screen's component. It is a regular `` with the safe area insets applied as extra padding or margin.
```tsx
import { Text } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
export default function HomeScreen() {
return (
Content is in safe area.
);
}
```
Using a different Expo template and don't have Expo Router installed?
Import and add [`SafeAreaProvider`](https://appandflow.github.io/react-native-safe-area-context/api/safe-area-provider) to the root component file (such as **App.tsx**) before using `SafeAreaView` in your screen component.
```tsx
import { SafeAreaProvider } from 'react-native-safe-area-context';
export default function App() {
return (
return ...;
);
}
```
## Alternate: `useSafeAreaInsets` hook
Alternate to `SafeAreaView`, you can use [`useSafeAreaInsets`](https://appandflow.github.io/react-native-safe-area-context/api/use-safe-area-insets) hook in your screen component. It provides direct access to the safe area insets, allowing you to apply padding for each edge of the `` using an inset from this hook.
The example below uses the `useSafeAreaInsets` hook. It applies top padding to a `` using `insets.top`.
```tsx
import { Text, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
export default function HomeScreen() {
const insets = useSafeAreaInsets();
return (
Content is in safe area.
);
}
```
The hook provides the insets in the following object:
```ts
{
top: number,
right: number,
bottom: number,
left: number
}
```
## Additional information
### Minimal example
Below is a minimal working example that uses the `useSafeAreaInsets` hook to apply top padding to a view.
```tsx
import { Text, View } from 'react-native';
import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context';
function HomeScreen() {
const insets = useSafeAreaInsets();
return (
Content is in safe area.
);
}
export default function App() {
return (
);
}
```
### Usage with React Navigation
By default, React Navigation supports safe areas and uses `react-native-safe-area-context` as a peer dependency. For more information, see the [React Navigation documentation](https://reactnavigation.org/docs/handling-safe-area/).
### Usage with web
If you are targeting the web, set up `SafeAreaProvider` as described in the [usage section](/develop/user-interface/safe-areas#usage). If you are doing server-side rendering (SSR), see the [Web SSR section](https://appandflow.github.io/react-native-safe-area-context/optimizations#web-ssr) in the library's documentation.
---
---
modificationDate: February 25, 2026
title: System bars
description: Learn how to handle and customize system bars for safe areas and edge-to-edge layout in your Expo project.
---
# System bars
Learn how to handle and customize system bars for safe areas and edge-to-edge layout in your Expo project.
System bars are the UI elements at the edges of the screen that provide essential device information and navigation controls. Depending on the mobile OS, they include the status bar ([Android](https://developer.android.com/design/ui/mobile/guides/foundations/system-bars) and [iOS](https://developer.apple.com/design/human-interface-guidelines/status-bars)), caption bar ([Android](https://medium.com/androiddevelopers/insets-handling-tips-for-android-15s-edge-to-edge-enforcement-872774e8839b#:~:text=or%20SHORT_EDGES.-,Caption%20bars,-When%20your%20app) only), navigation bar ([Android](https://developer.android.com/design/ui/mobile/guides/foundations/system-bars#navigation-bar) and [iOS](https://developer.apple.com/design/human-interface-guidelines/navigation-bars)), and home indicator (iOS only).
These components are used to display device information such as battery level, time, notification alerts, and provide direct interaction with the device from anywhere in the device's interface. For example, an app user can pull down the status bar to access quick settings and notifications regardless of which app they're currently using.
System bars are fundamental to the mobile experience, and understanding how to work with them properly is important for creating your app.
## Handling overlaps using safe areas
Some of your app's content may draw behind the system bars. To handle this, you need to position your app's content correctly by avoiding the overlap and ensuring that the controls from the system bars are present.
The following guide walks you through how to use `SafeAreaView` or a hook to apply insets directly for each edge of the screen.
[Safe areas](/develop/user-interface/safe-areas) — Learn how to add safe areas for screen components inside your Expo project.
### Safe areas and edge-to-edge layout on Android
Before [edge-to-edge on Android](https://expo.dev/blog/edge-to-edge-display-now-streamlined-for-android), it was common to have a translucent status bar and navigation bar. With this approach, the content drawn behind these bars was already underneath them, and it was typically not necessary to factor in safe areas.
Now, [with edge-to-edge on Android](https://expo.dev/blog/edge-to-edge-display-now-streamlined-for-android), you will need to use safe areas to ensure that content does not overlap with system bars.
## Customizing system bars
System bars can be customized to match your app's design and provide better visibility in different scenarios. When using Expo, there are two libraries available for this: `expo-status-bar` and `expo-navigation-bar` (Android only).
### Status bar configuration
The status bar appears at the top of the screen on both Android and iOS. You can customize it using [`expo-status-bar`](/versions/latest/sdk/status-bar). It provides a `StatusBar` component that you can use to control the appearance of the status bar while your app is running using the [`style`](/versions/latest/sdk/status-bar#style) property or the [`setStatusBarStyle`](/versions/latest/sdk/status-bar#statusbarsetstatusbarstylestyle-animated) method:
```tsx
import { StatusBar } from 'expo-status-bar';
export default function RootLayout() {
<>
{/* Use light text instead of dark text in the status bar to provide more contrast with a dark background. */}
>;
}
```
> **Note:** In Expo default template, the `style` property is set to `auto`. It automatically picks the appropriate style depending on the color scheme (light or dark mode) currently used by your app.
To control the `StatusBar` visibility, you can set the [`hidden`](/versions/latest/sdk/status-bar#hidden) property to `true` or use the [`setStatusBarHidden`](/versions/latest/sdk/status-bar#statusbarsetstatusbarhiddenhidden-animation) method.
**With edge-to-edge enabled on Android, features from `expo-status-bar` that depend on an opaque status bar [are unavailable](https://developer.android.com/about/versions/15/behavior-changes-15#edge-to-edge)**. It's only possible to customize the style and visibility. Other properties will no-op and warn.
### Navigation bar configuration (Android only)
On Android devices, the Navigation Bar appears at the bottom of the screen. You can customize it using the [`expo-navigation-bar`](/versions/latest/sdk/navigation-bar) library. It provides a `NavigationBar` component that you can use to set the style of the navigation bar using the [`setStyle`](/versions/latest/sdk/navigation-bar#navigationbarsetstylestyle) method:
```tsx
import { Platform } from 'react-native';
import * as NavigationBar from 'expo-navigation-bar';
import { useEffect } from 'react';
useEffect(() => {
if (Platform.OS === 'android') {
// Set the navigation bar style
NavigationBar.setStyle('dark');
}
}, []);
```
To control the `NavigationBar` visibility, you can use the [`setVisibilityAsync`](/versions/latest/sdk/navigation-bar#navigationbarsetvisibilityasyncvisibility) method.
**With edge-to-edge enabled on Android, features from `expo-navigation-bar` that depend on an opaque navigation bar [are unavailable](https://developer.android.com/about/versions/15/behavior-changes-15#edge-to-edge)**. It's only possible to customize the style and visibility. Other properties will no-op and warn.
---
---
modificationDate: February 25, 2026
title: Fonts
description: Learn how to integrate custom fonts in your app using local files or Google Font packages
---
# Fonts
Learn how to integrate custom fonts in your app using local files or Google Font packages
Android and iOS come with their own set of platform fonts. To provide a consistent user experience and enhance your app's branding, you can use custom fonts.
This guide covers different ways you can add and load a custom font into your project and also provides additional information related to fonts.
## Add a custom font
There are two ways you can add a custom font into your project:
- Add a font file into your local assets. For example, a font file in the **assets/fonts** directory.
- Install a Google Font package. For example, installing [`@expo-google-fonts/inter`](https://www.npmjs.com/package/@expo-google-fonts/inter) package.
### Supported font formats
Expo SDK officially supports OTF and TTF font formats across Android, iOS and web platforms. If your font is in another font format, you have to set up advanced configuration to support that format in your project.
### Variable fonts
Variable fonts, including variable font implementations in OTF and TTF, do not have support across all platforms. For full platform support, use static fonts. Alternatively, use a utility such as [fontTools](https://fonttools.readthedocs.io/en/latest/varLib/mutator.html) to extract the specific axis configuration you want to use from the variable font and save it as a separate font file.
### How to choose between OTF and TTF
If the font you're using has both OTF and TTF versions, prefer OTF. The **.otf** files are smaller than **.ttf** files. Sometimes, OTF also renders slightly better in certain contexts.
## Use a local font file
Copy the file into your project's **assets/fonts** directory.
> **assets/fonts** directory path is a common convention in React Native apps to put font files. You can place these files elsewhere if you follow a custom convention.
Two ways to use the local font file in your project:
- Embed the font file with [`expo-font` config plugin](/versions/latest/sdk/font#configuration-in-app-config) (Android and iOS only).
- Load the font file with [`useFonts`](/versions/latest/sdk/font#usefontsmap) hook at runtime (Android, iOS, and web).
### With `expo-font` config plugin
The `expo-font` config plugin allows embedding one or more font files in your project's native code. It supports `ttf` and `otf` for both Android and iOS, and `woff` and `woff2` are supported on iOS only.
> **Note:** Config plugins only run on native platforms (Android and iOS). For web, use the [`useFonts` hook](/develop/user-interface/fonts#with-usefonts-hook) instead.
This is the recommended method for adding fonts to your app due to its benefits:
- Fonts are available immediately when the app starts on a device.
- No additional code required to load fonts in a project asynchronously when the app starts.
- Fonts are consistently available across all devices where the app is installed because they're bundled within the app.
However, this method also has some limitations:
- Doesn't work with Expo Go since this method requires [creating a development build](/develop/development-builds/create-a-build).
To embed a font in a project, follow the steps below:
After adding a custom font file in your project, install the `expo-font` library.
```sh
npx expo install expo-font
```
Add the config plugin to your [app config](/versions/latest/config/app#plugins) file. The configuration must contain the path to the font file using [`fonts`, `android` or `ios`](/versions/latest/sdk/font#configurable-properties) properties which take an array of one or more font definitions. The path to each font file is relative to the project's root.
The example below showcases all valid ways a font can be specified: as an array of objects that specify `fontFamily` and other properties, or an array of paths to font files.
For Android, you can specify the `fontFamily`, `weight`, and optionally `style` (defaults to `"normal"`), which will embed the fonts as native [XML resources](https://developer.android.com/develop/ui/views/text-and-emoji/fonts-in-xml). If you provide only the font file paths in an array, the file name becomes the font family name on Android. iOS always extracts the font family name from the font file itself.
If you plan to refer to fonts using just the `fontFamily`, provide an array of font paths (see `FiraSans-MediumItalic.ttf` below) and follow our [recommendation for file naming](/develop/user-interface/fonts#how-to-determine-which-font-family-name-to-use).
If you want to refer to fonts using a combination of `fontFamily`, `weight`, and `style`, provide an array of objects (see `Inter` below).
```json
{
"expo": {
"plugins": [
[
"expo-font",
{
"fonts": [
"./assets/fonts/FiraSans-MediumItalic.ttf"
],
"android": {
"fonts": [
{
"fontFamily": "Inter",
"fontDefinitions": [
{
"path": "./assets/fonts/Inter-BoldItalic.ttf",
"weight": 700,
"style": "italic"
},
{
"path": "./assets/fonts/Inter-Bold.ttf",
"weight": 700
}
]
}
]
},
"ios": {
"fonts": ["./assets/fonts/Inter-Bold.ttf", "./assets/fonts/Inter-BoldItalic.ttf"]
}
}
]
]
}
}
```
After embedding the font with the config plugin, create a [new development build](/develop/development-builds/create-a-build) and install it on your device or Android Emulator or iOS Simulator.
You can use the font with `` by specifying the `fontFamily` style prop. The examples below correspond to the fonts defined in the configuration above.
```tsx
Inter BoldInter Bold ItalicFira Sans Medium Italic
```
Using this method in an existing React Native project?
- **Android:** Copy font files to **android/app/src/main/assets/fonts**.
- **iOS:** See [Adding a Custom Font to Your App](https://developer.apple.com/documentation/uikit/text_display_and_fonts/adding_a_custom_font_to_your_app) in the Apple Developer documentation.
#### How to determine which font family name to use
- If you provide fonts as an array of file paths (as described above), on Android, the file name (without the extension) becomes the font family name. On iOS, the font family name is read from the font file itself. We recommend naming the font file same as its [PostScript name](/develop/user-interface/fonts#what-is-postscript-name-of-a-font) so the font family name is consistent on both platforms.
- If you use the object syntax, provide the "Family Name". This can be found in the Font Book app on macOS, [fontdrop.info](https://fontdrop.info/) or other programs.
What is PostScript name of a font file?
The **PostScript name** of a font file is a unique identifier assigned to the font that follows Adobe's PostScript standard. It is used by operating systems and apps to refer to the font. It is not a font's **display name**.
For example, Inter Black font file's PostScript name is `Inter-Black`.
_Screenshot from Font Book app on macOS._
### With `useFonts` hook
The `useFonts` hook from `expo-font` library allows loading the font file asynchronously. This hook keeps track of the loading state and loads the font when an app is initialized.
It works with all Expo SDK versions and with Expo Go. To load a font in a project using `useFonts` hook, follow the steps below:
After adding a custom font file in your project, install the `expo-font` and `expo-splash-screen` libraries.
```sh
npx expo install expo-font expo-splash-screen
```
The [`expo-splash-screen`](/versions/latest/sdk/splash-screen) library provides `SplashScreen` component that you can use to prevent rendering the app until the font is loaded and ready.
Map the font file using the `useFonts` hook in a top level component such as the root layout (**app/layout.tsx**) file in your project:
```tsx
import { useFonts } from 'expo-font';
import * as SplashScreen from 'expo-splash-screen';
import {useEffect} from 'react';
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const [loaded, error] = useFonts({
'Inter-Black': require('./assets/fonts/Inter-Black.otf'),
});
useEffect(() => {
if (loaded || error) {
SplashScreen.hideAsync();
}
}, [loaded, error]);
if (!loaded && !error) {
return null;
}
return (
...
)
}
```
Use the font on the `` by using `fontFamily` style prop in a React component:
```tsx
Inter Black
```
## Use Google Fonts
Expo has first-class support for all fonts listed in [Google Fonts](https://fonts.google.com/). They are available using [`@expo-google-fonts`](https://github.com/expo/google-fonts) library. With any of the font package from this library, you can quickly integrate that font and its variants.
Two ways to use a Google Font in your project:
- Embed the installed font with [`expo-font` config plugin](/versions/latest/sdk/font#configuration-in-appjsonappconfigjs).
- Load the installed font with [`useFonts`](/versions/latest/sdk/font#usefontsmap) hook at runtime asynchronously.
### With `expo-font` config plugin
> **Note:** Embedding a Google Font using `expo-font` config plugin has same benefits and limitations as embedding a custom font on your own. See [using a local font file with `expo-font` config plugin](/develop/user-interface/fonts#with-expo-font-config-plugin) for more information.
Install the font package. For example, to use Inter Black font, install the [`@expo-google-fonts/inter`](https://www.npmjs.com/package/@expo-google-fonts/inter) package with the command below.
```sh
npx expo install expo-font @expo-google-fonts/inter
```
Add the config plugin to your [app config](/versions/latest/config/app#plugins) file. The configuration must contain the path to the font file using [`fonts`](/versions/latest/sdk/font#configurable-properties) property which takes an array of one or more font files. The path to the font file is defined from the font package inside the `node_modules` directory. For example, if you have a font package named `@expo-google-fonts/inter`, then the name of the file is **Inter_900Black.ttf**.
```json
{
"plugins": [
[
"expo-font",
{
"fonts": ["node_modules/@expo-google-fonts/inter/900Black/Inter_900Black.ttf"]
}
]
]
}
```
After embedding the font with the config plugin, create a [new development build](/develop/development-builds/create-a-build) and install it on your device or Android Emulator or iOS Simulator.
On Android, you can use the font file name. For example, `Inter_900Black`. On iOS, use the font and its weight name ([PostScript name](/develop/user-interface/fonts#what-is-postscript-name-of-a-font)). The example below demonstrates how to use [`Platform`](https://reactnative.dev/docs/platform-specific-code#platform-module) to select the correct font family name for each platform:
```tsx
import { Platform } from 'react-native';
// Inside a React component:
Inter Black
```
### With `useFonts` hook
> **Note:** Loading a Google Font using `useFonts` hook has same benefits and limitations as embedding a custom font on your own. See [using a local font file with `useFonts` hook](/develop/user-interface/fonts#with-usefonts-hook) for more information.
Each google Fonts package provides the `useFonts` hook to load the fonts asynchronously. This hook keeps track of the loading state and loads the font when an app is initialized. The font package also imports the font file so you don't have to explicitly import it.
Install the Google Fonts package, `expo-font` and `expo-splash-screen` libraries.
```sh
npx expo install @expo-google-fonts/inter expo-font expo-splash-screen
```
The [`expo-splash-screen`](/versions/latest/sdk/splash-screen) library provides `SplashScreen` component that you can use to prevent rendering the app until the font is loaded and ready.
After installing the font package, map the font using the `useFonts` hook in a top level component such as the root layout (**app/layout.tsx**) file in your project:
```tsx
// Rest of the import statements
import { Inter_900Black, useFonts } from '@expo-google-fonts/inter';
import * as SplashScreen from 'expo-splash-screen';
import {useEffect} from 'react';
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const [loaded, error] = useFonts({
Inter_900Black,
});
useEffect(() => {
if (loaded || error) {
SplashScreen.hideAsync();
}
}, [loaded, error]);
if (!loaded && !error) {
return null;
}
return (
...
)
}
```
Use the font on the `` by using `fontFamily` style prop in a React component:
```tsx
Inter Black
```
## Additional information
### Minimal example
[expo-font usage](/versions/latest/sdk/font#usage) — expo-font — See usage section in Expo Fonts API reference for a minimal example of using a custom font.
### Beyond OTF and TTF
If your font is in format other than OTF or TTF, you have to [customize the Metro bundler configuration to include it as an extra asset](/guides/customizing-metro#adding-more-file-extensions-to-assetexts) for it to work. In some cases, rendering a font format that a platform doesn't support may cause your app to crash.
For reference, the following table provides the list formats that work on each native platform:
| Format | Android | iOS | Web |
| --- | --- | --- | --- |
| bdf | ✗ | ✗ | ✗ |
| dfont | ✓ | ✗ | ✗ |
| eot | ✗ | ✗ | ✓ |
| fon | ✗ | ✗ | ✗ |
| otf | ✓ | ✓ | ✓ |
| ps | ✗ | ✗ | ✗ |
| svg | ✗ | ✗ | ✓ |
| ttc | ✗ | ✗ | ✗ |
| ttf | ✓ | ✓ | ✓ |
| woff | ✗ | ✓ | ✓ |
| woff2 | ✗ | ✓ | ✓ |
### Platform built-in fonts
If you don't want to use a custom font by specifying a `fontFamily`, platform's default font will be used. Each platform has a set of built in fonts. On Android, the default font is Roboto. On iOS, it's SF Pro.
A platform's default font is usually easy-to-read. However, don't be surprised when the system default font is changed to use another font that is not easy to read. In this case, use your custom font so you have precise control over what the user will see.
### Handle `@expo/vector-icons` initial load
When the icons from `@expo/vector-icons` library load for the first time, they appear as invisible icons in your app. Once they load, they're cached for all the app's subsequent usage. To avoid showing invisible icons on your app's first load, preload during the initial loading screen with [`useFonts`](/versions/latest/sdk/font#usefontsmap). For example:
```tsx
import { useFonts } from 'expo-font';
import Ionicons from '@expo/vector-icons/Ionicons';
export default function RootLayout() {
useFonts([require('./assets/fonts/Inter-Black.otf', Ionicons.font)]);
return (
...
)
}
```
Now, you can use any icon from the `Ionicons` library in a React component:
```tsx
```
[Icons](/guides/icons) — Learn how to use various types of icons in your Expo app, including vector icons, custom icon fonts, icon images, and icon buttons.
### Loading a remote font directly from the web
> **If you're loading remote fonts, make sure they are being served from an origin with CORS properly configured**. If you don't do this, your remote font might not load properly on the web platform.
Loading fonts from a local asset is the safest way to load a font in your app. When including fonts as local assets, after you submit your app to the app stores, these fonts are bundled with the app download and will be available immediately. You don't have to worry about CORS or other potential issues.
However, loading a font file directly from web is done by replacing the `require('./assets/fonts/FontName.otf')` with the URL of your font as shown in the example below.
```tsx
import { useFonts } from 'expo-font';
import { Text, View, StyleSheet } from 'react-native';
export default function App() {
const [loaded, error] = useFonts({
'Inter-SemiBoldItalic': 'https://rsms.me/inter/font-files/Inter-SemiBoldItalic.otf?v=3.12',
});
if (!loaded || !error) {
return null;
}
return (
Inter SemiBoldItalicPlatform Default
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});
```
---
---
modificationDate: February 25, 2026
title: Assets
description: Learn about using static assets in your project, including images, videos, sounds, database files, and fonts.
---
# Assets
Learn about using static assets in your project, including images, videos, sounds, database files, and fonts.
A **static asset** is a file that is bundled with your app's binary (native binary). This file type is not part of your app's JavaScript bundle which contain your app's code. Common types of static assets include images, videos, sounds, database files for SQLite, and fonts. These assets can be served locally from your project or remotely over the network.
This guide covers different ways you can load and use static assets in your project and also provides additional information on how to optimize and minify assets.
## Serve an asset locally
When an asset is stored in your project's file system, it can be embedded in your app binary at build time or loaded at runtime. You can import it like a JavaScript module using `require` or `import` statements.
For example, to render an image called **example.png** in **App.js**, you can use `require` to import the image from the project's **assets/images** directory and pass it to the `` component:
```tsx
```
In the above example, the bundler reads the imported image's metadata and automatically provides the width and height. For more information, see [Static Image Resources](https://reactnative.dev/docs/images#static-image-resources).
Libraries such as `expo-image` and `expo-file-system` work similarly to the `` component with local assets.
### How are assets served locally
Locally stored assets are served over HTTP in development. They are automatically bundled into your app binary at the build time for production apps and served from disk on a device.
### Load an asset at build time with `expo-asset` config plugin
To load an asset at build time, you can use the [config plugin](/versions/latest/sdk/asset#example-appjson-with-config-plugin) from the `expo-asset` library. This plugin will embed the asset file in your native project.
Install the `expo-asset` library.
```sh
npx expo install expo-asset
```
Add the config plugin to your project's [app config](/versions/latest/config/app#plugins) file. The configuration must contain the path to the asset file using [`assets`](/versions/latest/sdk/asset#configurable-properties) property which takes an array of one or more files or directories to link to the native project.
The path to each asset file must be relative to your project's root since the app config file is located in the project's root directory.
```json
{
"expo": {
"plugins": [
[
"expo-asset",
{
"assets": ["./assets/images/example.png"]
}
]
]
}
}
```
After embedding the asset with the config plugin, [create a new development build](/develop/development-builds/create-a-build). Now, you can import and use the asset in your project without using a `require` or an `import` statement.
For example, the **example.png** is linked by the above config plugin. You can directly import it into your component and use its resource name as the URI. Note that when rendering assets without using `require`, you need to explicitly provide a width / height.
```tsx
import { Image } from 'expo-image';
...
export default function HomeScreen() {
return ;
}
```
> Different file formats are supported with the `expo-asset` config plugin. For more information on these formats, see [Assets API reference](/versions/latest/sdk/asset#configurable-properties). If you don't see a file format supported by the config plugin, you can use the [`useAssets`](/develop/user-interface/assets#load-an-asset-at-runtime-with-useassets-hook) hook to load the asset at runtime.
### Load an asset at runtime with `useAssets` hook
The `useAssets` hook from `expo-asset` library allows loading assets asynchronously. This hook downloads and stores an asset locally and after the asset is loaded, it returns a list of that asset's instances.
Install the `expo-asset` library.
```sh
npx expo install expo-asset
```
Import the [`useAssets`](/versions/latest/sdk/asset#useassetsmoduleids) hook from the `expo-asset` library in your screen component:
```tsx
import { useAssets } from 'expo-asset';
export default function HomeScreen() {
const [assets, error] = useAssets([
require('path/to/example-1.jpg'),
require('path/to/example-2.png'),
]);
return assets ? : null;
}
```
## Serve an asset remotely
When an asset is served remotely, it is not bundled into the app binary at build time. You can use the URL of the asset resource in your project if it is hosted remotely. For example, pass the URL to the `` component to render a remote image:
```jsx
import { Image } from 'expo-image';
...
function App() {
return (
);
}
```
There is no guarantee about the availability of images served remotely using a web URL because an internet connection may not be available, or the asset might be removed.
Additionally, loading assets remotely also requires you to provide an asset's metadata. In the above example, since the bundler cannot retrieve the image's width and height, those values are passed explicitly to the `` component. If you don't, the image will default to 0px by 0px.
## Additional information
### Manual optimization methods
#### Images
You can compress images using the following:
- [`guetzli`](https://github.com/google/guetzli)
- [`pngcrush`](https://pmt.sourceforge.io/pngcrush/)
- [`optipng`](http://optipng.sourceforge.net/)
Some image optimizers are lossless. They re-encode your image to be smaller without any change or loss in the pixels displayed. When you need each pixel to be untouched from the original image, a lossless optimizer and a lossless image format like PNG are a good choice.
Other image optimizers are lossy. The optimized image differs from the original image. Often, lossy optimizers are more efficient because they discard visual information that reduces file size while making the image look nearly identical to humans. Tools like `imagemagick` can use comparison algorithms like [SSIM](https://en.wikipedia.org/wiki/Structural_similarity) to show how similar two images look. It's quite common for an optimized image that is over 95% similar to the original image to be far less than 95% of the original file size.
#### Other assets
For assets like GIFs or videos, or non-code and non-image assets, it's up to you to optimize and minify those assets.
> **Note**: GIFs are a very inefficient format. Modern video codecs can produce significantly smaller file sizes with better quality.
### Fonts
See [Add a custom font](/develop/user-interface/fonts#add-a-custom-font) for more information on how to add a custom font to your app.
---
---
modificationDate: February 25, 2026
title: Color themes
description: Learn how to support light and dark modes in your app.
---
# Color themes
Learn how to support light and dark modes in your app.
It's common for apps to support light and dark color schemes. Here is an example of how supporting both modes looks in an Expo project:
## Configuration
> For Android and iOS projects, additional configuration is required to support switching between light and dark mode. For web, no additional configuration is required.
To configure supported appearance styles, you can use the [`userInterfaceStyle`](/versions/latest/config/app#userinterfacestyle) property in your project's [app config](/versions/latest/config/app). By default, this property is set to `automatic` when you create a new project with the [default template](/get-started/create-a-project).
Here is an example configuration:
```json
{
"expo": {
"userInterfaceStyle": "automatic"
}
}
```
You can also configure `userInterfaceStyle` property for a specific platforms by setting either [`android.userInterfaceStyle`](/versions/latest/config/app#userinterfacestyle-2) or [`ios.userInterfaceStyle`](/versions/latest/config/app#userinterfacestyle-1) to the preferred value.
> The app will default to the `light` style if this property is absent.
When you are creating a development build, you have to install [`expo-system-ui`](/versions/latest/sdk/system-ui#installation) to support the appearance styles for Android. Otherwise, the `userInterfaceStyle` property is ignored.
```sh
npx expo install expo-system-ui
```
If the project is misconfigured and doesn't have `expo-system-ui` installed, the following warning will be shown in the terminal:
```sh
» android: userInterfaceStyle: Install expo-system-ui in your project to enable this feature.
```
You can also use the following command to check if the project is misconfigured:
```sh
npx expo config --type introspect
```
Using bare React Native app?
#### Android
Ensure that the `uiMode` flag is present on your `MainActivity` (and any other activities where this behavior is desired) in **AndroidManifest.xml**:
```xml
```
Implement the `onConfigurationChanged` method in **MainActivity.java**:
```java
import android.content.Intent;
import android.content.res.Configuration;
public class MainActivity extends ReactActivity {
...
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Intent intent = new Intent("onConfigurationChanged");
intent.putExtra("newConfig", newConfig);
sendBroadcast(intent);
}
...
}
```
#### iOS
You can configure supported styles with the [`UIUserInterfaceStyle`](https://developer.apple.com/documentation/bundleresources/information_property_list/uiuserinterfacestyle) key in your app **Info.plist**. Use `Automatic` to support both light and dark modes.
### Supported appearance styles
The `userInterfaceStyle` property supports the following values:
- `automatic`: Follow system appearance settings and notify about any change the user makes.
- `light`: Restrict the app to support light theme only.
- `dark`: Restrict the app to support dark theme only.
## Detect the color scheme
To detect the color scheme in your project, use `Appearance` or `useColorScheme` from `react-native`:
```tsx
import { Appearance, useColorScheme } from 'react-native';
```
Then, you can use `useColorScheme()` hook as shown below:
```tsx
function MyComponent() {
let colorScheme = useColorScheme();
if (colorScheme === 'dark') {
// render some dark thing
} else {
// render some light thing
}
}
```
In some cases, you will find it helpful to get the current color scheme imperatively with [`Appearance.getColorScheme()` or listen to changes with `Appearance.addChangeListener()`](https://reactnative.dev/docs/appearance).
## Additional information
### Minimal example
```tsx
import { Text, StyleSheet, View, useColorScheme } from 'react-native';
import { StatusBar } from 'expo-status-bar';
export default function App() {
const colorScheme = useColorScheme();
const themeTextStyle = colorScheme === 'light' ? styles.lightThemeText : styles.darkThemeText;
const themeContainerStyle =
colorScheme === 'light' ? styles.lightContainer : styles.darkContainer;
return (
Color scheme: {colorScheme}
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
text: {
fontSize: 20,
},
lightContainer: {
backgroundColor: '#d0d0c0',
},
darkContainer: {
backgroundColor: '#242c40',
},
lightThemeText: {
color: '#242c40',
},
darkThemeText: {
color: '#d0d0c0',
},
});
```
### Tips
While you are developing your project, you can change your simulator's or device's appearance by using the following shortcuts:
- If using an Android Emulator, you can run `adb shell "cmd uimode night yes"` to enable dark mode, and `adb shell "cmd uimode night no"` to disable dark mode.
- If using a physical Android device or an Android Emulator, you can toggle the system dark mode setting in the device's settings.
- If working with an iOS emulator locally, you can use the Cmd ⌘ + Shift + a shortcut to toggle between light and dark modes.
---
---
modificationDate: February 25, 2026
title: Animation
description: Learn how to integrate React Native animations and use it in your Expo project.
---
# Animation
Learn how to integrate React Native animations and use it in your Expo project.
Animations are a great way to enhance and provide a better user experience. In your Expo projects, you can use the [Animated API](https://reactnative.dev/docs/next/animations) from React Native. However, if you want to use more advanced animations with better performance, you can use the [`react-native-reanimated`](https://docs.swmansion.com/react-native-reanimated/) library. It provides an API that simplifies the process of creating smooth, powerful, and maintainable animations.
## Installation
You can skip installing `react-native-reanimated` if you have created a project using [the default template](/get-started/create-a-project). This library is already installed. Otherwise, install it by running the following command:
```sh
npx expo install react-native-reanimated
```
## Usage
### Minimal example
The following example shows how to use the `react-native-reanimated` library to create a simple animation. For more information on the API and advanced usage, see [`react-native-reanimated` documentation](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/your-first-animation).
```tsx
import Animated, {
useSharedValue,
withTiming,
useAnimatedStyle,
Easing,
} from 'react-native-reanimated';
import { View, Button, StyleSheet } from 'react-native';
export default function AnimatedStyleUpdateExample() {
const randomWidth = useSharedValue(10);
const config = {
duration: 500,
easing: Easing.bezier(0.5, 0.01, 0, 1),
};
const style = useAnimatedStyle(() => {
return {
width: withTiming(randomWidth.value, config),
};
});
return (
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
box: {
width: 100,
height: 80,
backgroundColor: 'black',
margin: 30,
},
});
```
## Other animation libraries
You can use other animation packages such as [Moti](https://moti.fyi/) in your Expo project. It works on Android, iOS, and web.
---
---
modificationDate: February 25, 2026
title: Store data
description: Learn about different libraries available to store data in your Expo project.
---
# Store data
Learn about different libraries available to store data in your Expo project.
Storing data can be essential to the features implemented in your mobile app. There are different ways to save data in your Expo project depending on the type of data you want to store and the security requirements of your app. This page lists a variety of libraries to help you decide which solution is best for your project.
## Expo SecureStore
`expo-secure-store` provides a way to encrypt and securely store key-value pairs locally on the device.
[Expo SecureStore API reference](/versions/latest/sdk/securestore) — For more information on how to install and use expo-secure-store, see its API documentation.
## Expo FileSystem
`expo-file-system` provides access to a file system stored locally on the device. Within Expo Go, each project has a separate file system and no access to other Expo projects' files. However, it can save content shared by other projects to the local filesystem and share local files with other projects. It is also capable of uploading and downloading files from network URLs.
[Expo FileSystem API reference](/versions/latest/sdk/filesystem) — For more information on how to install and use expo-file-system, see its API documentation.
## Expo SQLite
`expo-sqlite` package gives your app access to a database that can be queried through a WebSQL-like API. The database is persisted across restarts of your app. You can use it for importing an existing database, opening databases, creating tables, inserting items, querying and displaying results, and using prepared statements.
[Expo SQLite API reference](/versions/latest/sdk/sqlite) — For more information on how to install and use expo-sqlite, see its API documentation.
## Async Storage
[Async Storage](https://react-native-async-storage.github.io/async-storage/) is an asynchronous, unencrypted, persistent key-value storage for React Native apps. It has a simple API and is a good choice for storing small amounts of data. It is also a good choice for storing data that does not need encryption, such as user preferences or app state.
[Async Storage documentation](https://react-native-async-storage.github.io/async-storage/docs/usage) — For more information on how to install and use Async Storage, see its documentation.
## Other libraries
There are other libraries available for storing data for different purposes. For example, you might not need encryption in your project or are looking for a faster solution similar to Async Storage.
We recommend checking out [React Native for a list of libraries](https://reactnative.directory/?search=storage) to help you store your project's data.
---
---
modificationDate: February 25, 2026
title: Next steps
description: A list of useful resources to learn more about implementing navigation and UI in your app.
---
# Next steps
A list of useful resources to learn more about implementing navigation and UI in your app.
[Use TypeScript](/guides/typescript) — An in-depth guide on configuring an Expo project with TypeScript or migrating an existing JavaScript project.
[Icons](/guides/icons) — Learn how to use various types of icons in your Expo app, including vector icons, custom icon fonts, icon images, and icon buttons.
[ESLint and Prettier](/guides/using-eslint) — A guide on configuring ESLint and Prettier to format Expo projects.
---
---
modificationDate: February 25, 2026
title: Introduction to development builds
description: Why use development builds and how to get started.
---
# Introduction to development builds
Why use development builds and how to get started.
**Development build** is the term that we use for a "Debug" build of an app that includes the [`expo-dev-client`](/versions/latest/sdk/dev-client) library. This library augments the built-in React Native development tooling with additional capabilities, such as support for inspecting network requests and a "launcher" UI that lets you switch between different development servers (such as between a server running on your machine or a teammate's machine) and deployments of your app (such as published updates with EAS Update).
Difference between Expo Go and development builds
[Expo Go](https://expo.dev/go) is a playground app for students and learners to get started quickly. It comes with a fixed set of native libraries built in, so you can write JavaScript code and see changes instantly without building a native app yourself. A development build is a fully featured development environment for working on your production-grade Expo apps.
Native app and JavaScript bundle
The **native app** is what you install on your device. Expo Go is a pre-built native app that works like a playground — it can't be changed after you install it. To add new native libraries or change things like your app name and icon, you need to build your own native app (a development build).
The **JavaScript bundle (`npx expo start`)** is where your app's UI code and business logic are. In production apps, there is one **main.js** bundle that is shipped with the app itself. In development, this JS bundle is live reloaded from your local machine. The main role of React Native is to provide a way for the JavaScript code to access the native APIs (Image, Camera, Notifications, and more). However, only APIs and libraries that were bundled in the **native app** can be used.
[Expo Go & Development Builds: which should you use?](https://www.youtube.com/watch?v=FdjczjkwQKE) — In this tutorial video Beto explains what each of them is, and when to choose a development build.
## Why use a development build (a.k.a what _can't_ you do in Expo Go and why)
Expo Go is a playground for students and learners to understand the basics of React Native. It's limited and not useful for building production-grade projects, so most apps will convert to using development builds. It helps to know exactly what is _impossible_ in Expo Go and _why_, so you can make an informed decision on when and why to make this move.
Use libraries with native code that aren't in Expo Go
Consider [`react-native-webview`](/versions/latest/sdk/webview) as an example, a library that contains native code, but [is included in Expo Go](https://github.com/expo/expo/blob/main/apps/expo-go/package.json#L23). When you run `npx expo install react-native-webview` command in your project, it will install the library in your **node_modules** directory, which includes both the JS code and the native code. But the JS bundle you are building _only_ uses the JS code. Then, your JS bundle gets uploaded to Expo Go, and it interacts with the native code that was already bundled with the app.
Instead, when you try to use a library that is not included, for example, [`react-native-firebase`](/guides/using-firebase#using-react-native-firebase), then you can use the JS code and hot reload the new bundle into Expo Go but it will immediately error because the JS code tries to call the native code from the React Native Firebase package that does not exist in Expo Go. There is no way to get the native code into the Expo Go app unless it was already included in the bundle that was uploaded to the app stores.
Test changes in app icon, name, splash screen
If you're developing your app in Expo Go only, you can build a store version that will use your provided values and images; it just won't be possible to test it in Expo Go.
These native assets are shipped with the native bundle and are immutable once the app is installed. The Expo Go app does show a splash screen, which is your app icon on a solid color background. This is a dev-only emulation to view how the splash screen will probably look. However, it is limited, for example, you cannot test `SplashScreen.setOptions` to animate the splash screen.
Remote push notifications
While [in-app notifications](/versions/latest/sdk/notifications) are available in Expo Go, remote push notifications (that is, sending a push notification from a server to the app) are not. This is because a push notification service should be tied to your own push notification certificates, and while it is possible to make it work in Expo Go, it often causes confusion for production builds. It is recommended to test remote push notifications in development builds so you can ensure parity in behavior between development and production.
Implementing App/Universal links
Both [Android App Links](/linking/android-app-links) and [iOS Universal Links](/linking/ios-universal-links) require a two-way association between the native app and the website. In particular, it requires the native app to include the linked website's URL. This is impossible with Expo Go due to the aforementioned native code immutability.
Open projects using older SDKs (iOS device only)
Expo Go can only support one SDK version at a time. When a new SDK version is released, Expo Go is updated to support the newer version, and this will be the only version of Expo Go available to install from the stores.
If you're developing on an Android Device, Android Emulator, or iOS Simulator, a compatible version of Expo Go can be [downloaded and installed](https://expo.dev/go). The only platform where this is impossible is iPhone devices because Apple does not support side-loading older versions of apps.
[Expo Go to development build](/develop/development-builds/expo-go-to-dev-build) — Learn how to migrate an existing Expo Go project to using development builds
[Local app development](/guides/local-app-development) — How to build a development client on your local machine
[Development builds on EAS](/develop/development-builds/create-a-build) — How to build a development client on EAS
---
---
modificationDate: February 25, 2026
title: Switch from Expo Go to a development build
description: How to switch from your Expo Go project to use development builds.
---
# Switch from Expo Go to a development build
How to switch from your Expo Go project to use development builds.
To switch from Expo Go to a development build, you'll need to follow the steps below:
## Install the `expo-dev-client`
The Expo Dev Client library includes the launcher UI (shown in the screenshots below), dev menu, extensions to test over-the-air updates, and more. The Expo Go app has the dev menu built in, and that's why you need to install it separately for a development build.
```sh
npx expo install expo-dev-client
```
When you run a development build it will look like this, only with your app name and icon included rather than "Microfoam". The launcher UI is pictured in iOS on the left and Android on the right. In between, you can see an app running inside of the development build, with the customizable developer menu open.
> We recommend using the `expo-dev-client` for the best development experience, but it is possible to use development builds without installing this library. If not using the dev client, in [Step 3](/develop/development-builds/expo-go-to-dev-build#start-the-dev-client), start the bundler with `--dev-client`. Otherwise, it will default to opening in Expo Go.
## Build your native app
With Expo Go, you only needed to build the JavaScript bundle, but with development builds you also need to compile the native app. With Expo, there are two parts to building your native app:
1. Generate the native **android** and/or **ios** directories ([read more](/develop/development-builds/expo-go-to-dev-build#prebuild) on when and how you'll need to do this)
2. Use native build tools to compile the native app(s)
Once you've built your native app, you won't need to build it again unless you add or update a library with native code, or change any native code or configuration, such as the app name.
> The **android** and **ios** directories are automatically added to **.gitignore** when you create a new project, so they won't be checked into Git. This ensures you can always regenerate the code locally or on CI with [CNG](/workflow/continuous-native-generation) when needed and never have to edit native code manually.
### Option 1: Build on your local machine
To build a native app on your local machine, follow the setup your environment guides for [Android](/workflow/android-studio-emulator) and [iOS](/workflow/ios-simulator) platforms. This involves setting up and configuring native build tools like Android Studio for Android and Xcode for iOS.
Once you have everything set up, run the following command:
```sh
npx expo run:android
```
By default, this will build and install the app on an Android Emulator/iOS Simulator. If you need to run the build on your phone, plug it into your computer (on Android, select trust device and allow USB debugging if prompted, and on iOS, enable [developer mode](/get-started/set-up-your-environment?mode=development-build&buildEnv=local&platform=ios&device=physical#plug-in-your-device-via-usb-and-enable-developer-mode)) and run the above commands with the `--device` flag.
### Option 2: Build on EAS
Building on EAS servers is useful when:
- You can't or don't want to set up your local development environment
- You want to build an iOS app but don't own a Mac
- You want to share the development builds with your team
[Build on EAS](/develop/development-builds/create-a-build) — How to create your Development Build on EAS
## Start the bundler
After building locally, `npx expo run:android|ios` will start the bundler automatically. But if you closed the bundler or are working on a dev client you built earlier, (re)start the Metro bundler with:
```sh
npx expo start
```
When your project has `expo-dev-client` installed, the bundler will print out **Using development build**, and the QR code it shows will link into the development build you created instead of Expo Go.
## Prebuild
[**Prebuild**](/workflow/continuous-native-generation#prebuild) is a concept unique to Expo projects. It refers to the process of generating the **android** and **ios** directories based on your local configuration and properties.
### When should you run prebuild
You will need to run prebuild locally if you are building via `npx expo run:android|ios`, and change any native dependencies or configuration, such as:
- Installing or updating a library containing native code
- Changing [app config](/workflow/configuration)(`app.json`)
- Upgrading your Expo SDK version
In these cases, you'll want to rebuild the native directories with:
```sh
npx expo prebuild --clean
```
Then, rebuild your app with the updated native code, with:
```sh
npx expo run:android
```
### When you don't need to run prebuild
All Expo build tools (`npx expo run:android|ios` and `eas build`) will **prebuild** automatically if no existing native folders are found. This means that there is no need to run prebuild manually when you're running `npx expo run:android|ios` for the first time or `eas build`.
[Continuous Native Generation (CNG)](/workflow/continuous-native-generation) — Learn about the philosophy and benefits of Continuous Native Generation (CNG) and Prebuild
---
---
modificationDate: February 25, 2026
title: Create a development build on EAS
description: Learn how to create development builds for a project.
---
# Create a development build on EAS
Learn how to create development builds for a project.
When you create a new Expo app with `npx create-expo-app`, you start off with a project where you update the JavaScript code on your local machine and view the changes in the Expo Go app. A **development build** is essentially **your own version of Expo Go** where you are free to use any native libraries and change any native config. In this guide, you will learn how to convert your project that runs on Expo Go into a development build, which will make the native side of your app fully customizable.
[How to create a development build](https://www.youtube.com/watch?v=uQCE9zl3dXU)
## Prerequisites
The instructions assume you already have an existing Expo project that runs on Expo Go.
The requirements for building the native app depend on which platform you are using, which platform you are building for, and whether you want to build on EAS or on your local machine.
Build on EAS
This is the easiest way to build your native app, as it requires no native build tools on your side. The builds happen on the EAS servers, which makes it possible to trigger iOS builds from non-macOS platforms.
| | Android | iOS Simulator | iPhone device |
| --- | --- | --- | --- |
| **macOS** | ✓ | ✓ | ✓ (\*) |
| **Windows** | ✓ | ✓ | ✓ (\*) |
| **Linux** | ✓ | ✓ | ✓ (\*) |
(\*) All builds that run on an iPhone device require a paid [Apple Developer](https://developer.apple.com) account for build signing.
Build locally using the EAS CLI
Any EAS CLI command can be built on your local machine with the `--local` flag. This requires your local [development environment](https://reactnative.dev/docs/set-up-your-environment?os=macos&platform=ios) to be set up with native build tools. Read more about [local app development](/build-reference/local-builds).
| | Android | iOS Simulator | iPhone device |
| --- | --- | --- | --- |
| **macOS** | ✓ | ✓ | ✓ (\*) |
| **Windows** | ✓ (\*\*) | ✗ | ✗ |
| **Linux** | ✓ | ✗ | ✗ |
(\*) All builds that run on an iPhone device require a paid [Apple Developer](https://developer.apple.com) account for build signing.
(\*\*) No first-class support, but possible with [WSL](http://expo.fyi/wsl.md).
Build locally without EAS
To build locally without EAS requires your local [development environment](https://reactnative.dev/docs/set-up-your-environment?os=macos&platform=ios) to be set up with native build tools. This is the only way to test your iOS build on an iPhone device without a paid Apple Developer Account (only possible on macOS). Read more about [local app compilation](/guides/local-app-development#local-app-compilation) and see the [Expo Go to Development Build](/develop/development-builds/expo-go-to-dev-build) guide.
| | Android | iOS Simulator | iPhone device |
| --- | --- | --- | --- |
| **macOS** | ✓ | ✓ | ✓ |
| **Windows** | ✓ | ✗ | ✗ |
| **Linux** | ✓ | ✗ | ✗ |
## Get started
For detailed, step-by-step instructions, see our [EAS Tutorial](/tutorial/eas/introduction). Available also as a [tutorial series](https://www.youtube.com/playlist?list=PLsXDmrmFV_AS14tZCBin6m9NIS_VCUKe2) on YouTube.
### Install expo-dev-client
```sh
npx expo install expo-dev-client
```
Are you using this library in a existing (bare) React Native apps?
Apps that don't use [Continuous Native Generation](/workflow/continuous-native-generation) or are created with `npx react-native`, require further configuration after installing this library. See steps 1 and 2 from [Install `expo-dev-client` in an existing React Native app](/bare/install-dev-builds-in-bare).
### Build the native app (Android)
Prerequisites
3 requirements
1.
Expo account
Sign up for an [Expo](https://expo.dev/signup) account, if you haven't already.
2.
EAS CLI
The [EAS CLI](/build/setup#install-the-latest-eas-cli) installed and logged in.
```sh
npm install -g eas-cli && eas login
```
3.
An Android Emulator (optional)
An [Android Emulator](/workflow/android-studio-emulator) is optional if you want to test your app on an emulator.
```sh
eas build --platform android --profile development
```
Read more about [Android builds on EAS](/tutorial/eas/android-development-build).
### Build the native app (iOS Simulator)
Prerequisites
3 requirements
1.
Expo account
Sign up for an [Expo](https://expo.dev/signup) account, if you haven't already.
2.
EAS CLI
The [EAS CLI](/build/setup#install-the-latest-eas-cli) installed and logged in.
```sh
npm install -g eas-cli && eas login
```
3.
macOS with iOS Simulator installed
iOS Simulators are available only on macOS. Make sure you have the [iOS Simulator](/workflow/ios-simulator) installed.
Edit `development` profile in **eas.json** and set the [`simulator`](/eas/json#simulator) option to `true` (you have to create a separate profile for simulator builds if you also want to create iOS device builds for this project).
```json
{
"build": {
"development": {
"ios": {
"simulator": true
}
}
}
}
```
```sh
eas build --platform ios --profile development
```
iOS Simulator builds can only be installed on simulators and not on real devices.
Read more about [iOS Simulator builds on EAS](/tutorial/eas/ios-development-build-for-simulators).
### Build the native app (iOS device)
Prerequisites
3 requirements
1.
Expo account
Sign up for an [Expo](https://expo.dev/signup) account, if you haven't already.
2.
EAS CLI
The [EAS CLI](/build/setup#install-the-latest-eas-cli) installed and logged in.
```sh
npm install -g eas-cli && eas login
```
3.
Apple Developer account
A paid [Apple Developer](https://developer.apple.com/) account for creating [signing credentials](/app-signing/managed-credentials#generating-app-signing-credentials) so the app could be installed on an iOS device.
```sh
eas build --platform ios --profile development
```
iOS device builds can only be installed on iPhone devices and not on iOS Simulators.
Read more about [iOS device builds on EAS](/tutorial/eas/ios-development-build-for-devices).
### Install the app
You'll need to install the native app on your device, emulator, or simulator.
#### When building on EAS
If you create your development build on EAS, the CLI will prompt you to install the app after the build is finished. You can also install previous builds from the [expo.dev](https://expo.dev/) dashboard or using [Expo Orbit](https://expo.dev/orbit).
#### When building locally using EAS CLI
When building locally the output of the build will be an archive. You may drag and drop this on your Android Emulator/iOS Simulator to install it, or use [Expo Orbit](https://expo.dev/orbit) to install a build from your local machine.
### Start the bundler
The development client built in **step 2** is the native side of your app (basically your own version of Expo Go). To continue developing, you'll also want to start the JavaScript bundler.
Depending on how you built the app, this may already be running, but if you close the process for any reason, there is no need to rebuild your development client. Simply restart the JavaScript bundler with:
```sh
npx expo start
```
This is the same command you would have used with Expo Go. It detects whether your project has `expo-dev-client` installed, in which case it will default to targeting your development build instead of Expo Go.
## Video walkthroughs
["EAS Tutorial Series"](https://www.youtube.com/playlist?list=PLsXDmrmFV_AS14tZCBin6m9NIS_VCUKe2) — A course on YouTube: learn how to speed up your development with Expo Application Services.
["Async Office Hours: How to make a development build with EAS Build"](https://www.youtube.com/watch?v=LUFHXsBcW6w) — Learn how to make a development build with EAS Build in this video tutorial hosted by Developer Success Engineer: Keith Kurak.
---
---
modificationDate: February 25, 2026
title: Use a development build
description: Learn how to use development builds for a project.
---
# Use a development build
Learn how to use development builds for a project.
Usually, creating a new native build from scratch takes long enough that you'll be tempted to switch tasks and lose your focus. However, with the development build installed on your device or an emulator/simulator, you won't have to wait for the native build process until you [change the underlying native code](/develop/development-builds/use-development-builds#rebuild-a-development-build) that powers your app.
## Start the development server
To start developing, run the following command to start the development server:
```sh
npx expo start
```
To open the project inside your development client:
- Press a or i keys to open your project on an Android Emulator or an iOS Simulator.
- On a physical device, scan the QR code from your system's camera or a QR code reader to open the project on your device.
## The launcher screen
If you launch the development build from your device's Home screen, you will see your launcher screen, which looks similar to the following:
If a bundler is detected on your local network, or if you have signed in to an Expo account in both Expo CLI and your development build, you can connect to it directly from this screen. Otherwise, you can connect by scanning the QR code displayed by the Expo CLI.
## Rebuild a development build
If you add a library to your project that contains native code APIs, for example, [`expo-secure-store`](/versions/latest/sdk/securestore), you will have to rebuild the development client. This is because the native code of the library is not included in the development client automatically when installing the library as a dependency on your project.
## Debug a development build
When you need to, you can access the menu by pressing Cmd ⌘ + d or Ctrl + d in Expo CLI or by shaking your phone or tablet. Here you'll be able to access all of the functions of your development build, any debugging functionality you need, or switch to a different version of your app.
See [Debugging](/debugging/runtime-issues) guide for more information.
---
---
modificationDate: February 25, 2026
title: Share a development build with your team
description: Learn how to install and share the development with your team or run it on multiple devices.
---
# Share a development build with your team
Learn how to install and share the development with your team or run it on multiple devices.
Android and iOS both offer ways to install a build of your application directly on devices. This gives you full control of putting specific builds on devices, allowing you to iterate quickly and have multiple builds of your application available for review at the same time. You can also share it with your team or run it on multiple test devices.
## Share the URL
When a development build is ready, a shareable URL is generated for your build with instructions on how to get it up and running. You can use this URL with a teammate or send it to your test device to install the build. The URL generated is unique to the build for your project.
> If you register any new iOS devices after creating a development build, you'll need to create a new development build to install it on those devices. For more information, see [internal distribution](/build/internal-distribution).
### Use the EAS dashboard
You can also direct your teammate to the build page in the EAS dashboard. From there, they can download the build artifact directly on their device.
### Use EAS CLI
Your teammate can also download and install the development build using EAS CLI. They have to make sure that they are signed from the Expo account associated with the development build and then can run the following command:
```sh
eas build:run --profile development
```
If the profile name for the development build is different from `development`, use it instead with `--profile`.
### iOS-only instructions
> If you're running iOS 16 or above and haven't yet turned on Developer Mode, you'll need to [enable it](/guides/ios-developer-mode) before you can run your build. (This doesn't apply if you're using enterprise provisioning.)
You can use `eas build:resign` to codesign an existing **.ipa** for iOS to a new ad hoc provisioning profile. This helps reduce time when distributing with your team. For example, if you want to add a new test device to an existing build, you can use this command to update the provisioning profile to include the device without rebuilding the entire app from scratch. For more information, see [Re-signing new credentials](/app-signing/app-credentials#re-signing-new-credentials).
## Next steps
[Install multiple app variants on the same device](/build-reference/variants) — Learn how to install multiple variants (development, preview, production) of an app on the same device side by side by converting app.json to app.config.js and additional configuration that is required to start the development server for each variant. — app.json — app.config.js
[Sharing pre-release versions of your app](/guides/sharing-preview-releases) — Learn more about sharing pre-release versions of your app.
---
---
modificationDate: February 25, 2026
title: Tools, workflows and extensions
description: Learn more about different tools, workflows and extensions available when working with development builds.
---
# Tools, workflows and extensions
Learn more about different tools, workflows and extensions available when working with development builds.
Development builds allow you to iterate quickly. However, you can extend the capabilities of your development build to provide a better developer experience when working in teams or customize the build to suit your needs.
## Tools
### Tunnel URLs
Sometimes, restrictive network conditions make it difficult to connect to the development server. The `npx expo start` command exposes your development server on a publicly available URL that is accessible through firewalls from around the globe. This option is helpful if you are not able to connect to your development server with the default LAN option or if you want to get feedback on your implementation while you are developing.
To get a tunneled URL, pass the [`--tunnel` flag](/more/expo-cli#tunneling) to `npx expo start` from the command line.
### Published updates
EAS CLI's `eas update` command bundles the current state of your JavaScript and asset files into an optimized "update". This update is stored on a hosting service by Expo. A development build of your app can load published updates without needing to check out a particular commit or leave a development machine running.
### Manually entering an update's URL
When a development build launches, it will expose UI to load a development server, or to "Enter URL manually". You can provide a URL manually that will launch a specific branch. The URL follows this pattern:
```text
https://u.expo.dev/[your-project-id]?channel-name=[channel-name]
# Example
https://u.expo.dev/F767ADF57-B487-4D8F-9522-85549C39F43F?channel-name=main
```
To get your project's ID, use the URL in the [app config's `expo.updates.url`](/versions/latest/config/app#url) field. To see a list of channels, run `eas channel:list`.
### Deep linking to an update's URL
You can load your app on a device that has a compatible build of your custom client by opening a URL of the form `{scheme}://expo-development-client/?url={manifestUrl}`. You'll need to pass the following parameters:
| parameter | value |
| --- | --- |
| `scheme` | URL scheme of your client (defaults to `exp+{slug}` where [`slug`](/versions/latest/config/app#slug) is the value set in the app config) |
| `manifestUrl` | URL-encoded URL of an update manifest to load. The URL will be `https://u.expo.dev/[your-project-id]?channel-name=[channel-name]` |
Example:
```text
exp+app-slug://expo-development-client/?url=https%3A%2F%2Fu.expo.dev%2F767ADF57-B487-4D8F-9522-85549C39F43F%2F%3Fchannel-name%3Dmain
```
In the example above, the `scheme` is `exp+app-slug`, and the `manifestUrl` is a project with an ID of `F767ADF57-B487-4D8F-9522-85549C39F43F` and a channel of `main`.
#### Using updates deep links in automation scenarios
When launching an update URL in a development build on an emulator or simulator using automation, such as in a CI/CD workflow, you can add the `disableOnboarding=1` query parameter to the URL to skip the onboarding screen that appears on the first launch of a development build after installation.
#### App-specific deep links
When testing deep links in your development build, such as when navigating to a specific screen in an Expo Router app or testing redirecting back to your app during an Oauth login flow, construct the URL exactly as you would if you were deep-linking into a standalone build of your app (for example, `myscheme://path/to/screen`).
Your project must be already open in the development build for an app-specific deep link to work. Cold-launching a development build with an app-specific deep link is not currently supported. Avoid using `expo-development-client` in your app-specific deep links in the path, as it is a reserved path used for launching an updated URL.
### QR codes
You can use our endpoint to generate a QR code that can be easily loaded by a development build.
Requests send to `https://qr.expo.dev/development-client` when supplied the query parameters such as `appScheme` and `url` will receive a response with an SVG image containing a QR code that can be easily scanned to load a version of your project in your development build.
| parameter | value |
| --- | --- |
| `appScheme` | URL-encoded deeplinking scheme of your development build (defaults to `exp+{slug}` where [`slug`](/versions/latest/config/app#slug) is the value set in the app config) |
| `url` | URL-encoded URL of an update manifest to load. The URL will be `https://u.expo.dev/[your-project-id]?channel-name=[channel-name]` |
Example:
```text
https://qr.expo.dev/development-client?appScheme=exp%2Bapps-slug&url=https%3A%2F%2Fu.expo.dev%2FF767ADF57-B487-4D8F-9522-85549C39F43F0%3Fchannel-name%3Dmain
```
In the example above, the `scheme` is `exp+app-slug`, and the `url` is a project with an ID of `F767ADF57-B487-4D8F-9522-85549C39F43F` and a channel of `main`.
## Example workflows
These are a few examples of workflows to help your team get the most out of your development build. If you come up with others that would be useful for other teams, [submit a PR](https://github.com/expo/expo/tree/main/CONTRIBUTING.md#-updating-documentation) to share your knowledge!
### PR previews
You can set up your CI process to publish an EAS Update whenever a pull request is updated and add a QR code that is used to view the change in a compatible development build.
See [instructions for publishing app previews on pull requests](/eas-update/github-actions#publish-previews-on-pull-requests) to implement this workflow in your project using GitHub Actions or serve as a template in your CI of choice.
## Extensions
Extensions allow you to extend your development client with additional capabilities.
### Extending the dev menu
The dev menu can be extended to include extra buttons by using the `registerDevMenuItems` API:
```tsx
import { registerDevMenuItems } from 'expo-dev-menu';
const devMenuItems = [
{
name: 'My Custom Button',
callback: () => console.log('Hello world!'),
},
];
registerDevMenuItems(devMenuItems);
```
This will create a new section in the dev menu that includes the buttons you have registered:
> Subsequent calls of `registerDevMenuItems` will override all previous entries.
### EAS Update
The EAS Update extension provides the ability to view and load published updates in your development client.
It's available for all development clients `v0.9.0` and above. To install it, you'll need the most recent publish of `expo-updates`:
```sh
npx expo install expo-dev-client expo-updates
```
#### Configure EAS Update
If you have not yet configured EAS Updates in your project, you can find [additional instructions on how to do so here.](/eas-update/getting-started)
You can now view and load EAS Updates in your development build via the `Extensions` panel.
## Set runtimeVersion in app config
When you create a development build of your project, you'll get a stable environment to load any changes to your app that are defined in JavaScript or other asset-related changes. Other changes to your app, whether defined directly in **android** and **ios** directories or by packages or SDKs you choose to install, will require you to create a new build of your development build.
To enforce an API contract between the JavaScript and native layers of your app, you should set the [`runtimeVersion`](/eas-update/runtime-versions) value in the app config. Each build you make will have this value embedded and will only load bundles with the same `runtimeVersion`, in both development and production.
---
---
modificationDate: February 25, 2026
title: Next steps
description: A list of useful resources to learn more about development builds and EAS Build.
---
# Next steps
A list of useful resources to learn more about development builds and EAS Build.
[Configuring EAS Build with eas.json](/build/eas-json) — Learn how a project using EAS services is configured with eas.json.
[Environment variables](/guides/environment-variables) — Learn about different ways to use environment variables in an Expo project.
[Android build process](/build-reference/android-builds) — Learn how an Android project is built on EAS Build.
[iOS build process](/build-reference/ios-builds) — Learn how an iOS project is built on EAS Build.
[Set up EAS Build with a monorepo](/build-reference/build-with-monorepos) — Learn how to set up EAS Build with a monorepo.
---
---
modificationDate: February 25, 2026
title: Introduction to config plugins
description: An introduction to Expo config plugins.
---
# Introduction to config plugins
An introduction to Expo config plugins.
When using [Continuous Native Generation (CNG)](/workflow/continuous-native-generation) in a project, native project (**android** and **ios** directories) changes are implemented without directly interacting with the native project files. Instead, you can use a config plugin to automatically configure your native project beyond what can be configured using the default app config props.
## What is a config plugin
A config plugin is a top-level custom configuration point that is not built into the [app config](/workflow/configuration). Using a config plugin, you can modify native projects created during the [prebuild](/workflow/continuous-native-generation#usage) process in CNG projects.
A config plugin is referenced in the `plugins` property of the [app config](/workflow/configuration) file and is made up of one or more plugin functions. These plugin functions are written in JavaScript and are executed during the prebuild process.
## Glossary
A typical config plugin is made up of one or more plugin functions that work together. The following diagram shows how the different parts of a config plugin interact with each other:
```
withMyPlugin ("myPlugin") [Config Plugin]
→ withAndroidPlugin, withIosPlugin [Plugin Function]
→ withAndroidManifest, withInfoPlist [Mod Plugin Function]
→ mods.android.manifest, mods.ios.infoplist [Mod]
```
In the following guides, we will use the above diagram to highlight specific terminology explained below:
### Plugin
The top-level config plugin which is referenced in your app config's `plugins` array. This is the entry point for your plugin. Conventionally, it is named `with`. For example, `withMyPlugin`. It is made of one or more [plugin functions](/config-plugins/introduction#plugin-function).
### Plugin function
One or more functions inside a config plugin that are called _plugin functions_. They wrap the underlying logic of performing platform-specific modifications. Technically, plugin functions look just like the function for the top-level plugin itself, and could be used as a plugin independently. Breaking plugins into smaller functions is often helpful for testing and debugging.
### Mod plugin function
Wrapper functions from `expo/config-plugins` library that provide a safe way to modify native files using `mods`. As a developer, you will use these functions in your config plugin instead of underlying `mods`.
### Mod
The underlying platform-specific modifiers (like `mods.android.manifest` and `mods.ios.infoplist`) that directly modify native project files during prebuild.
## Why use a config plugin
Config plugins can add native configuration to your project that isn't included by default. They can be used to generate app icons, set the app name, configure **AndroidManifest.xml** and **Info.plist**, and so on.
In CNG projects, it is best to avoid modifying these native projects manually, because you cannot regenerate them safely without potentially overwriting manual modifications. Config plugins allow you to modify these native projects in a _predictable way_ by consolidating your native project changes into a configuration file and applying them when you run `npx expo prebuild` (either manually or automatically during a CI/CD process). For example, when you change the name of your app in app config and run `npx expo prebuild`, the name will change in your native projects automatically without the need to manually update **AndroidManifest.xml** and **Info.plist** files.
## Characteristics of a config plugin
Config plugins have the following characteristics:
- Plugins are **synchronous** functions that accept an [ExpoConfig](/workflow/configuration) and return a modified `ExpoConfig`. In rare cases, plugins can also be asynchronous if available methods to communicate with native projects are asynchronous, but they won't be performant.
- Plugins should be named using the following convention: `with`, for example, `withFacebook`
- Plugins should be synchronous and their return value should be serializable, except for adding any [`mods`](/config-plugins/introduction#mods)
- Plugins are always evaluated during the app config evaluation phase.
- Optionally, a second argument can be passed to the plugin to configure it
- Mods are only evaluated during the **syncing** phase of `npx expo prebuild` (prebuild process) and modify native files during code generation. Because of that, any modifications made to app config in a config plugin should be outside of a mod to ensure that they are executed in non-prebuild configuration scenarios.
## Get started
[Create a config plugin](/config-plugins/plugins) — Comprehensive guide on how to create and use config plugins in your Expo project.
[Mods](/config-plugins/mods) — Comprehensive guide on how mods work, how to create them, and their best practices.
[Best practices for development and debugging](/config-plugins/development-and-debugging) — Learn about best practices for development and debugging config plugins.
---
---
modificationDate: February 25, 2026
title: Create and use config plugins
description: Learn how to create and use a config plugins in your Expo project.
---
# Create and use config plugins
Learn how to create and use a config plugins in your Expo project.
This guide covers sections on how to create a config plugin, how to pass parameters to a config plugin, and how to chain multiple config plugins together. It also covers how to use a config plugin from an Expo library.
Using the diagram below, in this guide, you will learn the first two parts of the config plugin hierarchy:
```
withMyPlugin ("myPlugin") [Config Plugin]
→ withAndroidPlugin, withIosPlugin [Plugin Function]
→ withAndroidManifest, withInfoPlist [Mod Plugin Function]
→ mods.android.manifest, mods.ios.infoplist [Mod]
```
> **Note:** The following sections use dynamic [app config](/workflow/configuration) (**app.config.js/app.config.ts** instead of **app.json**), which is not required to use a simple config plugin. However, it is required to use dynamic app config when you want to create/use a function-based config plugin that accepts parameters.
## Creating a config plugin
In the following section, let's create a local config plugin that adds an arbitrary property `HelloWorldMessage` to the **AndroidManifest.xml** for Android and **Info.plist** for iOS.
This example will create and modify the following files. To follow along, create a **plugins** directory in the root of your project, and inside it, create **withAndroidPlugin.ts**, **withIosPlugins.ts**, and **withPlugin.ts** files.
`plugins`
`withAndroidPlugin.ts``Contains Android-specific modifications`
`withIosPlugin.ts``Contains iOS-specific modifications`
`withPlugin.ts``Main plugin file that combines both Android and iOS plugins`
`app.config.ts``Dynamic app config file that uses the plugin`
### Create Android plugin
In **withAndroidPlugin.ts**, add the following code:
```ts
import { ConfigPlugin, withAndroidManifest } from 'expo/config-plugins';
const withAndroidPlugin: ConfigPlugin = config => {
// Define a custom message
const message = 'Hello world, from Expo plugin!';
return withAndroidManifest(config, config => {
const mainApplication = config?.modResults?.manifest?.application?.[0];
if (mainApplication) {
// Ensure meta-data array exists
if (!mainApplication['meta-data']) {
mainApplication['meta-data'] = [];
}
// Add the custom message as a meta-data entry
mainApplication['meta-data'].push({
$: {
'android:name': 'HelloWorldMessage',
'android:value': message,
},
});
}
return config;
});
};
export default withAndroidPlugin;
```
The example code above adds a meta-data entry `HelloWorldMessage` to the **android/app/src/main/AndroidManifest.xml** file by importing `ConfigPlugin` and `withAndroidManifest` from the `expo/config-plugins` library. The [`withAndroidManifest`](/config-plugins/mods#mod-plugins) mod plugin is an asynchronous function that accepts a config and a data object and modifies the value before returning an object.
### Create iOS plugin
In **withIosPlugin.ts**, add the following code:
```ts
import { ConfigPlugin, withInfoPlist } from 'expo/config-plugins';
const withIosPlugin: ConfigPlugin = config => {
// Define the custom message
const message = 'Hello world, from Expo plugin!';
return withInfoPlist(config, config => {
// Add the custom message to the Info.plist file
config.modResults.HelloWorldMessage = message;
return config;
});
};
export default withIosPlugin;
```
The example code above adds `HelloWorldMessage` as the custom key with a custom message in **ios//Info.plist** file by importing the `ConfigPlugin` and `withInfoPlist` from the `expo/config-plugins` library. The [`withInfoPlist`](/config-plugins/mods#mod-plugins) mod plugin is an asynchronous function that accepts a config and a data object and modifies the value before returning an object.
### Create a combined plugin
Now you can create a combined plugin that applies both platform-specific plugins. This approach allows the maintenance of platform-specific code separately while providing a single entry point.
In **withPlugin.ts**, add the following code:
```ts
import { ConfigPlugin } from 'expo/config-plugins';
import withAndroidPlugin from './withAndroidPlugin';
import withIosPlugin from './withIosPlugin';
const withPlugin: ConfigPlugin = config => {
// Apply Android modifications first
config = withAndroidPlugin(config);
// Then apply iOS modifications and return
return withIosPlugin(config);
};
export default withPlugin;
```
### Add TypeScript support and convert to dynamic app config
We recommend writing config plugins in TypeScript, since this will provide intellisense for the configuration objects. However, your app config is ultimately evaluated by Node.js, which does not recognize TypeScript code by default. Therefore, you will need to add a parser for the TypeScript files from the **plugins** directory to **app.config.ts** file.
Install `tsx` library by running the following command:
```sh
npm install --save-dev tsx
```
Then, change the static app config (**app.json**) to the [dynamic app config (**app.config.ts**)](/workflow/configuration#dynamic-configuration) file. You can do this by renaming the **app.json** file to **app.config.ts** and changing the content of the file as shown below. Ensure to add the following import statement at the top of your **app.config.ts** file:
```ts
import 'tsx/cjs';
module.exports = () => {
... rest of your app config
};
```
### Call the config plugin from your dynamic app config
Now, you can call the config plugin from your dynamic app config. To do this, you need to add the path to the **withPlugin.ts** file to the plugins array in your app config:
```ts
import "tsx/cjs";
import { ExpoConfig } from "expo/config";
module.exports = ({ config }: { config: ExpoConfig }) => {
... rest of your app config
plugins: [
["./plugins/withPlugin.ts"],
],
};
```
To see the custom config applied in native projects, run the following command:
```sh
npx expo prebuild --clean --no-install
```
To verify the custom config plugins applied, open **android/app/src/main/AndroidManifest.xml** and **ios//Info.plist** files:
```xml
```
```xml
HelloWorldMessageHello world, from Expo plugin!
```
## Passing a parameter to a config plugin
Your config plugin can accept parameters passed from your app config. To do so, you will need to read the parameter in your config plugin function, and then pass an object containing the parameter along with the config plugin function in your app config.
Considering the previous example, let's pass a custom message to the plugin. Add an `options` object in **withAndroidPlugin.ts** and update the `message` variable to use the `options.message` property:
```ts
...
type AndroidProps = {
message?: string;
};
const withAndroidPlugin: ConfigPlugin = (
config,
options = {}
) => {
const message = options.message || 'Hello world, from Expo plugin!';
return withAndroidManifest(config, config => {
... rest of the example remains unchanged
});
};
export default withAndroidPlugin;
```
Similarly, add an `options` object in **withIosPlugin.ts** and update the `message` variable to use the `options.message` property:
```ts
...
type IosProps = {
message?: string;
};
const withIosPlugin: ConfigPlugin = (config, options = {}) => {
const message = options.message || 'Hello world, from Expo plugin!';
... rest of the example remains unchanged
};
export default withIosPlugin;
```
Update the **withPlugin.ts** file to pass the `options` object to both plugins:
```ts
...
const withPlugin: ConfigPlugin<{ message?: string }> = (config, options = {}) => {
config = withAndroidPlugin(config, options);
return withIosPlugin(config, options);
};
```
To pass a value dynamically to the plugin, you can pass an object with the `message` property to the plugin in your app config:
```ts
{
...
plugins: [
[
"./plugins/withPlugin.ts",
{ message: "Custom message from app.config.ts" },
],
],
}
```
## Chaining config plugins
Config plugins can be chained together to apply multiple modifications. Each plugin in the chain runs in the order it appears, with the output of one plugin becoming the input for the next. This sequential execution ensures that dependencies between plugins are respected and allows you to control the precise order of modifications to your native code.
To chain config plugins, you can pass an array of plugins to the `plugins` array property in your app config. This is also supported in JSON app config file format (**app.json**).
```ts
module.exports = ({ config }: { config: ExpoConfig }) => {
name: 'my app',
plugins: [
[withFoo, 'input 1'],
[withBar, 'input 2'],
[withDelta, 'input 3'],
],
};
```
The `plugins` array uses `withPlugins` method under the hood to chain the plugins. If your plugins array is getting long or has complex configuration, you can use the `withPlugins` method directly to make your configuration easier to read. `withPlugins` will chain the plugins together and execute them in order.
```ts
import { withPlugins } from 'expo/config-plugins';
// Create a base config object
const baseConfig = {
name: 'my app',
... rest of the config
};
// ❌ Hard to read
withDelta(withFoo(withBar(config, 'input 1'), 'input 2'), 'input 3');
// ✅ Easy to read
withPlugins(config, [
[withFoo, 'input 1'],
[withBar, 'input 2'],
// When no input is required, you can just pass the method
withDelta,
]);
// Export the base config with plugins applied
module.exports = ({ config }: { config: ExpoConfig }) => {
return withPlugins(baseConfig, plugins);
};
```
## Using a config plugin
Expo config plugins are usually included in Node.js modules. You can install them just like other libraries in your project.
For example, `expo-camera` has a plugin that adds camera permissions to the **AndroidManifest.xml** and **Info.plist**. To install it in your project, run the following command:
```sh
npx expo install expo-camera
```
In your [app config](/versions/latest/config/app), you can add `expo-camera` to the list of plugins:
```json
{
"expo": {
"plugins": ["expo-camera"]
}
}
```
Some config plugins offer flexibility by allowing you to pass options to customize their configuration. To do this, you can pass an array with the Expo library name as the first argument, and an object containing the options as the second argument. For example, the `expo-camera` plugin allows you to customize the camera permission message:
```json
{
"expo": {
"plugins": [
[
"expo-camera",
{
"cameraPermission": "Allow $(PRODUCT_NAME) to access your camera."
}
]
]
}
}
```
> **Tip**: For every Expo library that has a config plugin, you'll find more information about that in the library's API reference. For example, the [`expo-camera` library has a config plugin section](/versions/latest/sdk/camera#configuration-in-appjsonappconfigjs).
On running the `npx expo prebuild`, the [`mods`](/config-plugins/introduction#mods) are compiled, and the native files change.
The changes don't take effect until you rebuild the native project, for example, with Xcode. **If you're using config plugins in a project without native directories (CNG projects), they will be applied during the prebuild step in EAS Build** or when running `npx expo prebuild|android|ios` locally.
---
---
modificationDate: February 25, 2026
title: Mods
description: Learn about mods and how to use them when creating a config plugin.
---
# Mods
Learn about mods and how to use them when creating a config plugin.
This guide explains what mods and mod plugins are, how they work, and how to use them effectively when creating config plugins for your Expo project.
Using the diagram below, in this guide, you will learn the last two parts of the config plugin hierarchy:
```
withMyPlugin ("myPlugin") [Config Plugin]
→ withAndroidPlugin, withIosPlugin [Plugin Function]
→ withAndroidManifest, withInfoPlist [Mod Plugin Function]
→ mods.android.manifest, mods.ios.infoplist [Mod]
```
## Mod plugins
Mod plugins provide a way to modify native project files during the prebuild process. They are made available from `expo/config-plugins` library and wrap top-level mods (also known as _default [mods](/config-plugins/mods#mods)_) because top-level mods are platform-specific and perform various tasks that can be difficult to understand at first.
> **Tip:** If you are developing a feature that requires mods, you should use _mod plugins_ instead of interacting with top-level mods directly.
### Available mod plugins
The following mod plugins are available in the `expo/config-plugins` library:
#### Android
| Default Android mod | Mod plugin | Dangerous | Description |
| --- | --- | --- | --- |
| `mods.android.manifest` | `withAndroidManifest` ([Example](https://github.com/expo/expo/blob/main/packages/expo-notifications/plugin/src/withNotificationsAndroid.ts)) | - | Modify the **android/app/src/main/AndroidManifest.xml** as JSON (parsed with [`xml2js`](https://www.npmjs.com/package/xml2js)) |
| `mods.android.strings` | `withStringsXml` ([Example](https://github.com/expo/expo/blob/d7fb5d254d5cb57ab06055136db72b9347d3db1e/packages/expo-navigation-bar/plugin/src/withNavigationBar.ts)) | - | Modify the **android/app/src/main/res/values/strings.xml** as JSON (parsed with [`xml2js`](https://www.npmjs.com/package/xml2js)). |
| `mods.android.colors` | `withAndroidColors` ([Example](https://github.com/expo/expo/blob/main/packages/%40expo/config-plugins/src/android/StatusBar.ts#L8)) | - | Modify the **android/app/src/main/res/values/colors.xml** as JSON (parsed with [`xml2js`](https://www.npmjs.com/package/xml2js)). |
| `mods.android.colorsNight` | `withAndroidColorsNight` ([Example](https://github.com/expo/expo/blob/main/packages/%40expo/prebuild-config/src/plugins/unversioned/expo-splash-screen/withAndroidSplashStyles.ts#L5)) | - | Modify the **android/app/src/main/res/values-night/colors.xml** as JSON (parsed with [`xml2js`](https://www.npmjs.com/package/xml2js)). |
| `mods.android.styles` | `withAndroidStyles` ([Example](https://github.com/expo/expo/blob/main/packages/%40expo/prebuild-config/src/plugins/unversioned/expo-splash-screen/withAndroidSplashStyles.ts#L5)) | - | Modify the **android/app/src/main/res/values/styles.xml** as JSON (parsed with [`xml2js`](https://www.npmjs.com/package/xml2js)). |
| `mods.android.gradleProperties` | `withGradleProperties` ([Example](https://github.com/expo/expo/blob/main/packages/%40expo/config-plugins/src/android/BuildProperties.ts#L5)) | - | Modify the **android/gradle.properties** as a `Properties.PropertiesItem[]`. |
| `mods.android.mainActivity` | `withMainActivity` ([Example](https://github.com/expo/expo/blob/main/packages/install-expo-modules/src/plugins/android/withAndroidModulesMainActivity.ts#L2)) | | Modify the **android/app/src/main//MainActivity.java** as a string. |
| `mods.android.mainApplication` | `withMainApplication` ([Example](https://github.com/expo/expo/blob/main/packages/expo-web-browser/plugin/src/withWebBrowserAndroid.ts#L8)) | | Modify the **android/app/src/main//MainApplication.java** as a string. |
| `mods.android.appBuildGradle` | `withAppBuildGradle` ([Example](https://github.com/expo/expo/blob/main/packages/%40expo/config-plugins/src/android/GoogleServices.ts#L5)) | | Modify the **android/app/build.gradle** as a string. |
| `mods.android.projectBuildGradle` | `withProjectBuildGradle` ([Example](https://github.com/expo/expo/blob/main/packages/%40expo/config-plugins/src/android/GoogleServices.ts#L5)) | | Modify the **android/build.gradle** as a string. |
| `mods.android.settingsGradle` | `withSettingsGradle` ([Example](https://github.com/expo/expo/blob/main/packages/install-expo-modules/src/plugins/android/withAndroidSettingsGradle.ts#L2)) | | Modify the **android/settings.gradle** as a string. |
#### iOS
| Default iOS mod | Mod plugin | Dangerous | Description |
| --- | --- | --- | --- |
| `mods.ios.infoPlist` | `withInfoPlist` ([Example](https://github.com/expo/expo/blob/main/packages/expo-location/plugin/src/withLocation.ts)) | - | Modify the **ios//Info.plist** as JSON (parsed with [`@expo/plist`](https://www.npmjs.com/package/@expo/plist)). |
| `mods.ios.entitlements` | `withEntitlementsPlist` ([Example](https://github.com/expo/expo/blob/main/packages/expo-apple-authentication/plugin/src/withAppleAuthIOS.ts)) | - | Modify the **ios//.entitlements** as JSON (parsed with [`@expo/plist`](https://www.npmjs.com/package/@expo/plist)). |
| `mods.ios.expoPlist` | `withExpoPlist` ([Example](https://github.com/expo/expo/blob/main/packages/%40expo/config-plugins/src/ios/Updates.ts#L6)) | - | Modify the **ios//Expo.plist** as JSON (Expo updates config for iOS) (parsed with [`@expo/plist`](https://www.npmjs.com/package/@expo/plist)). |
| `mods.ios.xcodeproj` | `withXcodeProject` ([Example](https://github.com/expo/expo/blob/main/packages/expo-asset/plugin/src/withAssetsIos.ts)) | - | Modify the **ios/.xcodeproj** as an `XcodeProject` object (parsed with [`xcode`](https://www.npmjs.com/package/xcode)). |
| `mods.ios.podfile` | `withPodfile` ([Example](https://github.com/expo/expo/blob/main/packages/%40expo/config-plugins/src/ios/Maps.ts#L6) | - | Modify the **ios/Podfile** as a string. |
| `mods.ios.podfileProperties` | `withPodfileProperties` ([Example](https://github.com/expo/expo/blob/main/packages/%40expo/config-plugins/src/ios/BuildProperties.ts#L4)) | - | Modify the **ios/Podfile.properties.json** as JSON. |
| `mods.ios.appDelegate` | `withAppDelegate` ([Example](https://github.com/expo/expo/blob/main/packages/%40expo/config-plugins/src/ios/Maps.ts#L6)) | | Modify the **ios//AppDelegate.m** as a string. |
> **Note about default Android and iOS mods:**
> Default mods are provided by the mod compiler for common file manipulation. Dangerous modifications rely on regular expressions (regex) to modify application code, which may cause the build to break. Regex mods are also difficult to version, and therefore should be used sparingly. Always opt toward using application code to modify application code, that is, [Expo Modules](https://github.com/expo/expo/tree/main/packages/expo-modules-core) native API.
## Mods
Config plugins use **mods** (short for modifiers) to modify native project files during the prebuild process. Mods are asynchronous functions that allow you to make changes to platform-specific files such as **AndroidManifest.xml** and **Info.plist**, and other native configuration files without having to manually edit them. They execute only during the **syncing** phase of `npx expo prebuild` (prebuild process).
They accept a config and a data object, then modify and return both of them as a single object. For example, in native projects, `mods.android.manifest` modifies **AndroidManifest.xml** and `mods.ios.plist` modifies **Info.plist**.
**You don't use mods as top-level functions (for example `with.android.manifest`) directly in your config plugin.** When you need to use a mod, you use _mod plugins_ in your config plugins. These mod plugins are provided by the `expo/config-plugins` library and wrap top-level mod functions and behind the scenes they perform various tasks. To see a list of available mods, check out the [mod plugins provided by `expo/config-plugins`](/config-plugins/mods#available-mod-plugins).
How default mods work and their key characteristics
When a default mod resolves, it is added to the `mods` object of the app config. This `mods` object is different from the rest of the app config because it doesn't get serialized, which means you can use it to perform actions _during_ code generation. Whenever possible, you should use available mod plugins instead of default mods since they are easier to work with.
Here is a high-level overview of how default mods work:
- The config is read using [`getPrebuildConfig`](https://github.com/expo/expo/blob/efc2db4eb1c909544e28792a15c89f8d22113c5b/packages/%40expo/prebuild-config/src/getPrebuildConfig.ts#L28) from `@expo/prebuild-config`
- All of the core functionality supported by Expo is added via plugins in `withIosExpoPlugins`. This includes name, version, icons, locales, and so on.
- The config is passed to the compiler `compileModsAsync`
- The compiler adds base mods that are responsible for reading data (like **Info.plist**), executing a named mod (like `mods.ios.infoPlist`), then writing the results to the file system
- The compiler iterates over all the mods and asynchronously evaluates them, providing some base props like the `projectRoot`
- After each mod, error handling asserts if the mod chain was corrupted by an invalid mod
Here are some key characteristics of default mods:
- `mods` are omitted from the manifest and **cannot** be accessed via `Updates.manifest`. Mods exist for the sole purpose of modifying native project files during code generation!
- `mods` can be used to read and write files safely during the `npx expo prebuild` command. This is how Expo CLI modifies the **Info.plist**, entitlements, xcproj, and so on.
- `mods` are platform-specific and should always be added to a platform-specific object:
```ts
module.exports = {
name: 'my-app',
mods: {
ios: {
/* iOS mods... */
},
android: {
/* Android mods... */
},
},
};
```
After mods are resolved, the contents of each mod will be written to disk. Custom mods can be added to support new native files. For example, you can create a mod to support the **GoogleServices-Info.plist**, and pass it to other mods.
### How mod plugins work
When a mod plugin is executed, it gets passed a `config` object with additional properties: `modResults` and `modRequest`.
#### `modResults`
The `modResults` object contains the data to modify and return. Its type depends on the mod that's being used.
#### `modRequest`
The `modRequest` object contains the following additional properties supplied by the mod compiler.
| Property | Type | Description |
| --- | --- | --- |
| `projectRoot` | `string` | Project root directory for the universal app. |
| `platformProjectRoot` | `string` | Project root for the specific platform. |
| `modName` | `string` | Name of the mod. |
| `platform` | `ModPlatform` | Name of the platform used in the mods config. |
| `projectName` | `string` | (iOS only) The path component used for querying project files. For example, `projectRoot/ios/[projectName]/`. |
## Create your own mod
For example, if you want to write a mod to update the Xcode Project's "product name", you'll create a config plugin file that uses the [`withXcodeProject`](/config-plugins/mods#ios) mod plugin.
```ts
import { ConfigPlugin, withXcodeProject, IOSConfig } from 'expo/config-plugins';
const withCustomProductName: ConfigPlugin = (config, customName) => {
return withXcodeProject(
config,
async (
config
) => {
config.modResults = IOSConfig.Name.setProductName({ name: customName }, config.modResults);
return config;
}
);
};
// Usage:
/// Create a config
const config = {
name: 'my app',
};
/// Use the plugin
export default withCustomProductName(config, 'new_name');
```
## Plugin module resolution
When implementing plugins, there are two fundamental approaches to consider:
1. **Plugins defined within your app's project**: These plugins live locally within your project, making them easy to customize and maintain alongside your app's code. They are ideal for project-specific customizations.
2. **Standalone package plugins**: These plugins exist as separate packages and are published to npm. This approach is ideal for reusable plugins that can be shared across multiple projects.
Both approaches provide the same capabilities for modifying your native configuration, but differ in how they're structured and imported. The sections below explain how module resolution works for each approach.
> Any resolution pattern that isn't specified below is unexpected behavior, and subject to breaking changes.
### Plugins defined within your app's project
With plugins defined within your app's project, you can implement plugins directly in your project in several ways:
#### File import
You can quickly create a plugin in your project by creating a JavaScript/TypeScript file and use it in your config like any other JS/TS file.
`app.config.ts``` `import "./my-config-plugin"` ``
`my-config-plugin.ts``✓ Imported from config`
In the above example, the config plugin file contains a bare minimum function:
```ts
module.exports = ({ config }: { config: ExpoConfig }) => {};
```
#### Inline function inside of dynamic app config
Expo config objects also support passing functions as-is to the `plugins` array. This is useful for testing, or if you want to use a plugin without creating a file.
```js
const withCustom = (config, props) => config;
const config = {
plugins: [
[
withCustom,
{
/* props */
},
],
withCustom,
],
};
```
One caveat to using functions instead of strings is that serialization will replace the function with the function's name. This keeps **manifests** (kind of like the **index.html** for your app) working as expected. Here is what the serialized config would look like:
```json
{
"plugins": [["withCustom", {}], "withCustom"]
}
```
### Standalone package plugins
> See [Create a module with a config plugin](/modules/config-plugin-and-native-module-tutorial) for a step-by-step guide on how to create a standalone package plugin.
Standalone package plugins can be implemented in two ways:
#### 1\. Dedicated config plugin packages
These are npm packages whose sole purpose is to provide a config plugin. For a dedicated config plugin package, you can export your plugin using `app.plugin.js`:
`app.config.ts``` `import "expo-splash-screen"` ``
`node_modules`
`expo-splash-screen``Node module`
`app.plugin.js``✓ Entry file for custom plugins`
`build`
`index.js``` ✗ Skipped in favor of `app.plugin.js` ``
#### 2\. Config plugins with companion packages
When a config plugin is part of a Node module without an **app.plugin.js**, it uses the package's `main` entry point:
`app.config.ts``` `import "expo-splash-screen"` ``
`node_modules`
`expo-splash-screen``Node module`
`package.json``` `"main": "./build/index.js"` ``
`build`
`index.js``✓ Node resolve to this file`
### Plugin resolution order
When you import a plugin package, files are resolved in this specific order:
1. **app.plugin.js in package root**
`app.config.ts``` `import "expo-splash-screen"` ``
`node_modules`
`expo-splash-screen``Node module`
`package.json``` `"main": "./build/index.js"` ``
`app.plugin.js``✓ Entry file for custom plugins`
`build`
`index.js``✗ Skipped in favor of app.plugin.js`
2. **Package's main entry (from package.json)**
`app.config.ts``` `import "expo-splash-screen"` ``
`node_modules`
`expo-splash-screen``Node module`
`package.json``` `"main": "./build/index.js"` ``
`build`
`index.js``✓ Node resolve to this file`
3. **Direct internal imports** (not recommended)
> Avoid importing module internals directly as it bypasses the standard resolution order and may break in future updates.
`app.config.ts``` `import "expo-splash-screen/build/index.js"` ``
`node_modules`
`expo-splash-screen`
`package.json``` `"main": "./build/index.js"` ``
`app.plugin.js``✗ Ignored due to direct import`
`build`
`index.js``` ✓ `expo-splash-screen/build/index.js` ``
### Why use app.plugin.js for plugins
The `app.plugin.js` approach is preferred for config plugins as it allows different transpilation settings from the main package code. This is particularly important because Node environments often require different transpilation presets compared to Android, iOS, or web JS environments (for example, `module.exports` instead of `import/export`).
---
---
modificationDate: February 25, 2026
title: Using a dangerous mod
description: Learn about dangerous mods and how to use them when creating a config plugin.
---
# Using a dangerous mod
Learn about dangerous mods and how to use them when creating a config plugin.
Dangerous mods in Expo provide direct access to native project files through string manipulation and regular expressions. While [existing mod plugins](/config-plugins/mods) are the recommended approach, dangerous mods serve as an escape hatch for modifications that cannot be achieved through existing mod plugins.
Why are they considered dangerous?
Automated direct source code manipulation does not typically compose well. For example, if a dangerous mod replaces text in a source file, and a subsequent dangerous mod expects the original text to be there (perhaps it uses the original text as an anchor for a regular expression) then it is unlikely produce the desired result — depending on how it is written, it may either throw an error or log. Other types of mods are less prone to this type of problem, although it can happen with mods that manipulate source files directly like `withAndroidManifest` and `withPodfile`.
Unlike standard mods, which can run multiple times safely, dangerous mods are rarely guaranteed to be idempotent. Running the same dangerous mod multiple times may produce different results, cause duplicate modifications, or break the target file entirely.
## When to use a dangerous mod
Consider using a dangerous mod when:
- **Can't make the modification with a standard mod**: The modification you need isn't supported by existing mod plugins like [`withAndroidManifest`](/config-plugins/mods#android), [`withPodfile`](/config-plugins/mods#ios), and so on, or if a library requires specific native modifications that aren't covered by standard plugins.
- **Legacy Expo SDK compatibility:** You are targeting an older Expo SDK version that doesn't include the mod plugin you need.
- **Need to modify text with regexes or replace functions**: You need to perform intricate text manipulations that existing mod plugins do not support. Expo uses dangerous mods internally for large file system refactoring, for example, when a library's name changes.
## How to use a dangerous mod
In a real-world scenario, you can use the example config plugin described in this section directly in your project by following the standard config plugin usage pattern from the [Creating a config plugin section](/config-plugins/plugins#creating-a-config-plugin). However, with the existing mod plugin called [`withPodfile`](/config-plugins/mods#ios), you don't have to use the dangerous mod. The example below is just for demonstration of how a dangerous mod can be created and used.
Let's take a look at an example config plugin to modify a file inside a native directory (**ios**). This is useful when you are using Continuous Native Generation in your Expo project. With the help of this config plugin, the native file (**ios/Podfile**) will update anytime the `npx expo prebuild` command runs, whether you run it manually or using EAS Build). This example is an ideal use case when an existing mod plugin cannot edit and update a file inside a native directory.
Following the directory structure and steps to create a config plugin (steps 3, 4, and 5) from [Creating a config plugin section](/config-plugins/plugins#creating-a-config-plugin), let's assume this config plugin is created inside the **plugins** directory of your Expo project:
```tsx
import { ConfigPlugin, IOSConfig, withDangerousMod } from 'expo/config-plugins';
import fs from 'fs/promises';
import path from 'path';
const withCustomPodfile: ConfigPlugin = config => {
return withDangerousMod(config, [
'ios',
async config => {
const podfilePath = path.join(config.modRequest.platformProjectRoot, 'Podfile');
try {
let contents = await fs.readFile(podfilePath, 'utf8');
const projectName = IOSConfig.XcodeUtils.getProjectName(config.modRequest.projectRoot);
contents = addCustomPod(contents, projectName);
await fs.writeFile(podfilePath, contents);
console.log('✅ Successfully added custom pod to Podfile');
} catch (error) {
console.warn('⚠️ Podfile not found, skipping modification');
}
return config;
},
]);
};
function addCustomPod(contents: string, projectName: string): string {
if (contents.includes("pod 'Alamofire'")) {
console.log('Alamofire pod already exists, skipping');
return contents;
}
const targetRegex = new RegExp(
`(target ['"]${projectName}['"] do[\\s\\S]*?use_expo_modules!)`,
'm'
);
return contents.replace(targetRegex, `$1\n pod 'Alamofire', '~> 5.6'`);
}
export default withCustomPodfile;
```
In the example above, the plugin **withCustomPodfile** will add a CocoaPod dependency automatically to your project's native **ios/Podfile** during the prebuild process. It uses `withDangerousMod` to provide access to the native file system directly and run after the native project is generated, but before any CocoaPod dependency is installed.
The **Podfile** requires direct text manipulation, which is done using a regex pattern inside `addCustomMod` function. This process also requires that the CocoaPod dependency is inserted into the **Podfile** at a specific location, which is after the `use_expo_modules!` statement.
## `withDangerousMod` syntax and requirements
Using `withDangerousMod` requires certain parameters:
1. A native platform (**android** or **ios**)
2. An asynchronous function that receives `config` object with file system access
3. Relative file name/path to access inside the native directory
4. Reading the existing file, modifying its contents, and writing back to the file
5. (Optional) Log custom messages for success and failure state when a plugin executes during the prebuild process
The code snippet below provides a skeleton of the required field and how the config plugin can be structured when using `withDangerousMod`:
```tsx
import { ConfigPlugin, withDangerousMod } from 'expo/config-plugins';
import fs from 'fs/promises';
import path from 'path';
const myPlugin: ConfigPlugin = config => {
return withDangerousMod(config, [
'platform', // 1. "ios" | "android"
async config => {
// 2. Async modification function
// 3. Build file paths
const filePath = path.join(
config.modRequest.platformProjectRoot, // Native project root
'path/to/file' // Relative path to target file
);
try {
// 4. Read existing file, modify its contents, and write back to the file
let contents = await fs.readFile(filePath, 'utf8');
contents = modifyContents(contents);
await fs.writeFile(filePath, contents);
// 5. Log success and failure states
console.log('✅ Successfully modified file');
} catch (error) {
console.warn('⚠️ File modification failed:', error);
}
return config;
},
]);
};
// Helper functions to use regex to modify the contents of the file
```
### Available paths in config plugins
Different path properties available in config plugins:
| Path | Type | Description |
| --- | --- | --- |
| `config.modRequest.projectRoot` | `string` | Universal app project root directory where **package.json** is located. Used for resolving assets, reading **package.json**, and cross-platform operations. Always verify the directory exists and contains **package.json**. |
| `config.modRequest.platformProjectRoot` | `string` | Platform-specific project root (**projectRoot/android** or **projectRoot/ios**). Used for platform-specific file operations like modifying native configuration files. Ensure the platform directory exists relative to main `projectRoot`. |
| `config.modRequest.projectName` | `string` | [iOS only] Project name component for constructing iOS file paths (for example, **projectRoot/ios/[projectName]/**). Used for iOS-specific file path construction. Only available on iOS platform and should match the actual Xcode project structure. |
| `config.modRequest.introspect` | `boolean` | Whether running in introspection mode where no filesystem changes should be made. When `true`, mods should only read and analyze files without writing. Used during config analysis and validation. |
| `config.modRequest.ignoreExistingNativeFiles` | `boolean` | Whether to ignore existing native files. Used in template-based operations, particularly affects entitlements and other native configs to ensure alignment with prebuild expectations. |
## Considerations when using a dangerous mod
When using a dangerous mod, consider the following:
- **Limited idempotency guarantees.** Unlike standard mods, which are generally idempotent and can work without the clean flag, dangerous mods are **rarely guaranteed to be idempotent**. This means running the same dangerous mod multiple times may produce different results or cause issues.
- **Experimental and prone to breakage.** Be careful using `withDangerousMod` as it is subject to change in the future. Test your dangerous mods thoroughly with each SDK release, as they are especially prone to breakage when native template changes occur.
- **Use standard mod plugins**. Both Android and iOS offer mod plugins like `withAndroidManifest`, `withPodfile`, `withPodfileProperties`, and so on, to perform common native file modifications. Only use a dangerous mod when there are no [existing mod plugins available](/config-plugins/mods#available-mod-plugins) to handle your use case.
- **Don't assume a file exists**. Always check the native directory and the relative path to the file before reading/writing to it. If you use CNG, you can always run `npx expo prebuild` to create native **android** and **ios** directories and manually verify a file's existence.
- **Dangerous mods run first**. The order in which dangerous mods execute might be unreliable since dangerous mods run before other modifiers. This can affect the predictability of your build process and may cause conflicts with other modifications.
---
---
modificationDate: February 25, 2026
title: Plugin development for libraries
description: Learn how to develop config plugins for Expo and React Native libraries.
---
# Plugin development for libraries
Learn how to develop config plugins for Expo and React Native libraries.
Expo config plugins in a React Native library represent a transformative approach to automating native project configuration. Rather than requiring library users to manually edit native files, such as **AndroidManifest.xml**, **Info.plist**, and so on, you can provide a plugin that handles these configurations automatically during the prebuild process. This changes developer experience from error-prone manual setup to reliable, automated configuration that can work consistently across different projects.
This guide explains key configuration steps and strategies that you can use to implement a config plugin in your library.
## Strategic value of a config plugin in a library
Config plugins tend to solve interconnected problems that have historically made React Native library adoption more difficult than it should be. At times, when a user installs a React Native library, they face a complex set of native configuration steps that must be performed correctly for the library to function. These steps are platform-specific and sometimes require deep knowledge of native development concepts.
By creating a config plugin within your library, you can transform this complex-looking manual process into a simple configuration declaration that a user can apply in their Expo project's app config file (usually, **app.json**). This reduces the barrier to adoption for your library and simultaneously makes the setup process reliable.
Beyond immediate user experience improvements, config plugins enable compatibility with [Continuous Native Generation](/workflow/continuous-native-generation), where native directories are generated automatically rather than checked into version control. Without a config plugin, developers who have adopted CNG face a difficult choice: either abandon the CNG workflow to manually configure native files, or invest significant effort in creating their own automation solutions. This creates a substantial barrier to library adoption in modern Expo development workflows.
## Project structure
A directory structure is the foundation for maintaining config plugins within your library. Below is an example directory structure:
`.`
`android``Android native module code`
`src`
`main`
`java`
`com`
`your-awesome-library`
`build.gradle`
`ios``iOS native module code`
`YourAwesomeLibrary`
`YourAwesomeLibrary.podspec`
`src`
`index.ts``Main library entry point`
`YourAwesomeLibrary.ts``Core library implementation`
`types.ts``TypeScript type definitions`
`plugin`
`src`
`index.ts``Plugin entry point`
`withAndroid.ts``Android-specific configurations`
`withIos.ts``iOS-specific configurations`
`build`
`__tests__`
`tsconfig.json``Plugin-specific TypeScript config`
`example`
`app.json``Example app configuration`
`App.tsx``Example app implementation`
`package.json``Example app dependencies`
`__tests__`
`app.plugin.js``Plugin entry point for Expo CLI`
`package.json``Package configuration`
`tsconfig.json``Main TypeScript configuration`
`jest.config.js``Testing configuration`
`README.md``Documentation`
The directory structure example above highlights the following organizational principles:
- **Root-level separation**: Clear boundaries between library code (**src**) and plugin implementation (**plugin**)
- **Plugin directory organization**: Platform-specific files (**withAndroid.ts**, **withIos.ts**) enable focused testing and maintenance
- **Build output management**: Compiled JavaScript and TypeScript declarations in **plugins/build/** directory
- **Testing**: Separate plugin tests from library tests to reflect different concerns.
## Installation and configuration for development
The most straightforward approach to leverage Expo's tooling is to use `expo` and [`expo-module-scripts`](https://www.npmjs.com/package/expo-module-scripts).
- `expo` provides a config plugin API and types that your plugin will use.
- `expo-module-scripts` provides build tooling specifically designed for Expo modules and config plugins. It also handles TypeScript compilation.
```sh
npx expo install package
```
When using `expo-module-scripts`, it requires the following **package.json** configuration. For any already existing script with the same script name, replace it.
```json
{
"scripts": {
"build": "expo-module build",
"build:plugin": "expo-module build plugin",
"clean": "expo-module clean",
"test": "expo-module test",
"prepare": "expo-module prepare",
"prepublishOnly": "expo-module prepublishOnly"
},
"devDependencies": {
"expo": "^54.0.0"
},
"peerDependencies": {
"expo": ">=54.0.0"
},
"peerDependenciesMeta": {
"expo": {
"optional": true
}
}
}
```
The next step is to add TypeScript support within the **plugins** directory. Open **plugins/tsconfig.json** file and add the following:
```json
{
"extends": "expo-module-scripts/tsconfig.plugin",
"compilerOptions": {
"outDir": "build",
"rootDir": "src"
},
"include": ["./src"],
"exclude": ["**/__mocks__/*", "**/__tests__/*"]
}
```
You also need to define the main entry point for your config plugin in the **app.plugin.js** file, which exports the compiled plugin code from the **plugin/build** directory:
```js
module.exports = require('./plugin/build');
```
The above configuration is essential because when the Expo CLI looks for a plugin, it checks for this file in the project root of your library. The **plugin/build** directory contains the JavaScript files generated from your config plugin's TypeScript source code.
## Key implementation patterns
Essential patterns for a successful config plugin implementation include:
- **Plugin structure**: Core patterns that every plugin should follow
- **Platform-specific implementations**: Handle Android and iOS configurations effectively
- **Test strategies:** Validating your plugin code through testing
### Plugin structure and platform-specific implementation
Every config plugin follows the same pattern: receives configuration and parameters, applies transformations through mods, and returns the modified configuration. Consider the following core plugin structure looks like:
```ts
import { type ConfigPlugin, withAndroidManifest, withInfoPlist } from 'expo/config-plugins';
export interface YourLibraryPluginProps {
customProperty?: string;
enableFeature?: boolean;
}
const withYourLibrary: ConfigPlugin = (config, props = {}) => {
// Apply Android configurations
config = withAndroidConfiguration(config, props);
// Apply iOS configurations
config = withIosConfiguration(config, props);
return config;
};
export default withYourLibrary;
```
### Testing strategies
Config plugin testing differs from regular library testing because you are testing configuration transformations rather than runtime behavior. Your plugin receives configuration objects and returns modified configuration objects.
Effective testing for a config plugin can be a combination of one or more of the following:
- **Unit testing:** Test configuration transformation logic with mocked Expo configuration objects
- **Cross-platform validation**: Use an example app to verify the actual prebuild output
- **Error condition testing**: Use error handling
Since unit tests focus on a plugin's transformation logic without involving the file system, you can use Jest to create and run mock configuration objects, pass them through your plugin, and verify expected modifications are made correctly. For example:
```ts
import { withYourLibrary } from '../src';
describe('withYourLibrary', () => {
it('should configure Android with custom property', () => {
const config = {
name: 'test-app',
slug: 'test-app',
platforms: ['android', 'ios'],
};
const result = withYourLibrary(config, {
customProperty: 'test-value',
});
// Verify the plugin was applied correctly
expect(result.plugins).toBeDefined();
});
});
```
Errors should be handled gracefully inside your config plugin to provide clear feedback when a configuration fails. Use `try-catch` blocks to intercept errors early:
```ts
const withYourLibrary: ConfigPlugin = (config, props = {}) => {
try {
// Validate configuration early
validateProps(props);
// Apply configurations
config = withAndroidConfiguration(config, props);
config = withIosConfiguration(config, props);
return config;
} catch (error) {
// Re-throw with more context if needed
throw new Error(`Failed to configure YourLibrary plugin: ${error.message}`);
}
};
```
## Alternative build approaches
If your library doesn't use `expo-module-scripts`, you have two options:
### Add a plugin to your main package
For libraries using different build tools (like those created with `create-react-native-library`), add an **app.plugin.js** file and build it along with your main package:
```js
module.exports = require('./lib/plugin');
```
### Create a separate plugin package
Some libraries distribute their config plugin as a separate package from their main library. This approach allows you to maintain your config plugin separately from the rest of your native module. You need to include export in **app.plugin.js** and compile the **build** directory from your plugin.
```js
{
"name": "your-library-expo-plugin",
"main": "app.plugin.js",
"files": ["app.plugin.js", "build/"],
"peerDependencies": {
"expo": "*",
"your-library": "*"
}
}
```
## Plugin development best practices
- **Instructions in your README**: If the plugin is tied to a React Native module, then you should document manual setup instructions for the package. If anything goes wrong with the plugin, developers should be able to manually add the project modifications that were automated by the plugin. This also allows you to support projects that are not using [CNG](/workflow/continuous-native-generation).
- Document the available properties for the plugin, specifying if any of the properties are required.
- If possible, plugins should be idempotent, meaning the changes they make are the same whether they are run on a fresh native project template or run again on a project template where its changes already exist. This allows developers to run `npx expo prebuild` without the `--clean` flag to sync changes to the config, rather than recreating the native project entirely. This may be more difficult with dangerous mods.
- **Naming conventions**: Use `withFeatureName` for the plugin function name if it applies to all platforms. If the plugin is platform-specific, use a camel case naming with the platform right after "with". For example, `withAndroidSplash`, `withIosSplash`.
- **Leverage built-in plugins**: If there's already a configuration available in [app config](/versions/latest/config/app) and [prebuild config](https://github.com/expo/expo/blob/main/packages/%40expo/prebuild-config/src/plugins/withDefaultPlugins.ts), you don't need to write a config plugin for it.
- **Split up plugins by platform**: When using functions within the config plugin, split them by platform. For example, `withAndroidSplash`, `withIosSplash`. This makes using the `--platform` flag in `npx expo prebuild` a bit easier to follow in `EXPO_DEBUG` mode, as the logging will show which platform-specific functions are being executed.
- **Unit test your plugin**: Write Jest tests for complex modifications. If your plugin requires access to the filesystem, use a mock system (we strongly recommend [`memfs`](https://www.npmjs.com/package/memfs)), you can see examples of this in the [`expo-notifications`](https://github.com/expo/expo/blob/fc3fb2e81ad3a62332fa1ba6956c1df1c3186464/packages/expo-notifications/plugin/src/__tests__/withNotificationsAndroid-test.ts#L34) plugin tests.
- Notice the root [\*\*/__mocks__/\*\*/\*](https://github.com/expo/expo/tree/main/packages/expo-notifications/plugin/__mocks__) directory and [**plugin/jest.config.js**](https://github.com/expo/expo/tree/main/packages/expo-notifications/plugin/jest.config.js).
- A TypeScript plugin is always preferable to a JavaScript due to added type-safety. Check out the [`expo-module-scripts` plugin](https://github.com/expo/expo/tree/main/packages/expo-module-scripts#-config-plugin) tooling for more info.
- Do not modify the `sdkVersion` via a config plugin, this can break commands like `expo install` and cause other unexpected issues.
---
---
modificationDate: February 25, 2026
title: Developing and debugging a plugin
description: Learn about development best practices and debugging techniques for Expo config plugins.
---
# Developing and debugging a plugin
Learn about development best practices and debugging techniques for Expo config plugins.
Developing a plugin is a great way to extend the Expo ecosystem. However, there are times you'll want to debug your plugin. This page provides some of the best practices for developing and debugging a plugin.
## Plugin development
> Use [modifier previews](https://github.com/expo/vscode-expo#expo-preview-modifier) to debug the results of your plugin live.
To make plugin development easier, we've added plugin support to [`expo-module-scripts`](https://www.npmjs.com/package/expo-module-scripts). Refer to the [config plugins guide](https://github.com/expo/expo/tree/main/packages/expo-module-scripts#-config-plugin) for more info on using TypeScript, and Jest to build plugins.
### Install dependencies
Use the following dependencies in a library that provides a config plugin:
```json
{
"dependencies": {},
"devDependencies": {
"expo": "^54.0.0"
},
"peerDependencies": {
"expo": ">=54.0.0"
},
"peerDependenciesMeta": {
"expo": {
"optional": true
}
}
}
```
- You may update the exact version of `expo` to build against a specific version.
- For simple config plugins that depend on core, stable APIs, such as a plugin that only modifies **AndroidManifest.xml** or **Info.plist**, you can use a loose dependency such as in the example above.
- You may also want to install [`expo-module-scripts`](https://github.com/expo/expo/blob/main/packages/expo-module-scripts/README.md) as a development dependency, but it's not required.
### Import the config plugins package
The `expo/config-plugins` and `expo/config` packages are re-exported from the `expo` package.
```js
const { ... } = require('expo/config-plugins');
const { ... } = require('expo/config');
```
Importing through the `expo` package ensures that you are using the version of the `expo/config-plugins` and `expo/config` packages that are depended on by the `expo` package.
If you do not import the package through the `expo` re-export in this way, you may accidentally be importing an incompatible version (depending on the implementation details of module hoisting in the package manager used by the developer consuming the module) or be unable to import the module at all (if using "plug and play" features of a package manager such as Yarn Berry or pnpm).
Config types are exported directly from `expo/config`, so there is no need to install or import from `expo/config-types`:
```ts
import { ExpoConfig, ConfigContext } from 'expo/config';
```
### Best practices for mods
- Avoid regex: [static modification](/config-plugins/development-and-debugging#static-modification) is key. If you want to modify a value in an Android gradle file, consider using `gradle.properties`. If you want to modify some code in the Podfile, consider writing to JSON and having the Podfile read the static values.
- Avoid performing long-running tasks like making network requests or installing Node modules in mods.
- Do not add interactive terminal prompts in mods.
- Generate, move, and delete new files in dangerous mods only. Failing to do so will break [introspection](/config-plugins/development-and-debugging#introspection).
- Utilize built-in config plugins like `withXcodeProject` to minimize the amount of times a file is read and parsed.
- Stick with the XML parsing libraries that prebuild uses internally, this helps prevent changes where code is rearranged needlessly.
## Plugin structure and scaffolding
### Versioning
By default, `npx expo prebuild` runs transformations on a [source template](https://github.com/expo/expo/tree/main/templates/expo-template-bare-minimum) associated with the Expo SDK version that a project is using. The SDK version is defined in the **app.json** or inferred from the installed version of `expo` that the project has.
When Expo SDK upgrades to a new version of React Native for instance, the template may change significantly to account for changes in React Native or new releases of Android or iOS.
If your plugin is mostly using [static modifications](/config-plugins/development-and-debugging#static-modification) then it will usually work well across SDK versions. If it's using a regular expression to transform application code, then you'll definitely want to document which Expo SDK version your plugin is intended for. During the SDK release cycle, there is a [beta period](https://github.com/expo/expo/blob/main/guides/releasing/Release%20Workflow.md#stage-4---beta-release) where you can test if your plugin works with the new version before it's released.
### Plugin properties
Properties are used to customize the way a plugin works during prebuild. They must always be static values (no functions, or promises). Consider the following types:
```ts
type StaticValue = boolean | number | string | null | StaticArray | StaticObject;
type StaticArray = StaticValue[];
interface StaticObject {
[key: string]: StaticValue | undefined;
}
```
Static properties are required because the app config must be serializable to JSON for use as the app manifest.
If possible, attempt to make your plugin work without props, this will help resolution tooling like [`expo install`](/config-plugins/development-and-debugging#expo-install) or [VS Code Expo Tools](https://marketplace.visualstudio.com/items?itemName=expo.vscode-expo-tools) work better. Remember that every property you add increases complexity, making it harder to change in the future and increases the amount of features you'll need to test. Good default values are preferred over mandatory configuration when feasible.
## Development environment
### Tooling
We highly recommend installing the [Expo Tools VS Code extension](https://marketplace.visualstudio.com/items?itemName=expo.vscode-expo-tools) as this will perform automatic validation on the plugins and surface error information along with other quality of life improvements for Config Plugin development.
### Set up a playground environment
You can develop plugins easily using JS, but if you want to set up Jest tests and use TypeScript, you will want a monorepo.
A monorepo will enable you to work on a node module and import it in your app config like you would if it were published to npm. Expo config plugins have full monorepo support built-in so all you need to do is set up a project.
In your monorepo's `packages/` directory, create a module, and [bootstrap a config plugin](https://github.com/expo/expo/tree/main/packages/expo-module-scripts#-config-plugin) in it.
### Manually run a plugin
If you aren't comfortable setting up a monorepo, you can try manually running a plugin:
- Run `npm pack` in the package with the config plugin
- In your test project, run `npm install path/to/react-native-my-package-1.0.0.tgz`, this will add the package to your **package.json** `dependencies` object.
- Add the package to the `plugins` array in your **app.json**: `{ "plugins": ["react-native-my-package"] }`
- If you have [VS Code Expo Tools](https://marketplace.visualstudio.com/items?itemName=expo.vscode-expo-tools) installed, autocomplete should work for the plugin.
- If you need to update the package, change the `version` in the package's **package.json** and repeat the process.
## Modifying native files with plugins
### Modify AndroidManifest.xml
Packages should attempt to use the built-in **AndroidManifest.xml** [merging system](https://developer.android.com/studio/build/manage-manifests) before using a config plugin. This can be used for static, non-optional features like permissions. This will ensure features are merged during build-time and not prebuild-time, which minimizes the possibility of the configuration being missed due to users forgetting to prebuild. The drawback is that users cannot use [introspection](/config-plugins/development-and-debugging#introspection) to preview the changes and debug any potential issues.
Here is an example of a package's **AndroidManifest.xml**, which injects a required permission:
```xml
```
If you're building a plugin for your local project, or if your package needs more control, then you should implement a plugin.
You can use built-in types and helpers to ease the process of working with complex objects. Here's an example of adding a `` to the default ``.
```ts
import { AndroidConfig, ConfigPlugin, withAndroidManifest } from 'expo/config-plugins';
import { ExpoConfig } from 'expo/config';
// Using helpers keeps error messages unified and helps cut down on XML format changes.
const { addMetaDataItemToMainApplication, getMainApplicationOrThrow } = AndroidConfig.Manifest;
export const withMyCustomConfig: ConfigPlugin = config => {
return withAndroidManifest(config, async config => {
// Modifiers can be async, but try to keep them fast.
config.modResults = await setCustomConfigAsync(config, config.modResults);
return config;
});
};
// Splitting this function out of the mod makes it easier to test.
async function setCustomConfigAsync(
config: Pick,
androidManifest: AndroidConfig.Manifest.AndroidManifest
): Promise {
const appId = 'my-app-id';
// Get the tag and assert if it doesn't exist.
const mainApplication = getMainApplicationOrThrow(androidManifest);
addMetaDataItemToMainApplication(
mainApplication,
// value for `android:name`
'my-app-id-key',
// value for `android:value`
appId
);
return androidManifest;
}
```
### Modify Info.plist
Using the `withInfoPlist` is a bit safer than statically modifying the `expo.ios.infoPlist` object in the **app.json** because it reads the contents of the Info.plist and merges it with the `expo.ios.infoPlist`, this means you can attempt to keep your changes from being overwritten.
Here's an example of adding a `GADApplicationIdentifier` to the **Info.plist**:
```ts
import { ConfigPlugin, withInfoPlist } from 'expo/config-plugins';
// Pass `` to specify that this plugin requires a string property.
export const withCustomConfig: ConfigPlugin = (config, id) => {
return withInfoPlist(config, config => {
config.modResults.GADApplicationIdentifier = id;
return config;
});
};
```
### Modify iOS Podfile
The iOS **Podfile** is the config file for CocoaPods, the dependency manager on iOS. It is similar to **package.json** for iOS. The **Podfile** is a Ruby file, which means you **cannot** safely modify it from Expo config plugins and should opt for another approach, such as [Expo Autolinking](/modules/autolinking) hooks.
We do expose one mechanism for safely interacting with the Podfile, but it's very limited. The versioned [template Podfile](https://github.com/expo/expo/tree/main/templates/expo-template-bare-minimum/ios/Podfile) is hard coded to read from a static JSON file **Podfile.properties.json**, we expose a mod (`ios.podfileProperties`, `withPodfileProperties`) to safely read and write from this file. This is used by [expo-build-properties](/versions/latest/sdk/build-properties) and to configure the JavaScript engine.
### Add plugins to `pluginHistory`
`_internal.pluginHistory` was created to prevent duplicate plugins from running while migrating from legacy UNVERSIONED plugins to versioned plugins.
```ts
import { ConfigPlugin, createRunOncePlugin } from 'expo/config-plugins';
// Keeping the name, and version in sync with it's package.
const pkg = require('my-cool-plugin/package.json');
const withMyCoolPlugin: ConfigPlugin = config => config;
// A helper method that wraps `withRunOnce` and appends items to `pluginHistory`.
export default createRunOncePlugin(
// The plugin to guard.
withMyCoolPlugin,
// An identifier used to track if the plugin has already been run.
pkg.name,
// Optional version property, if omitted, defaults to UNVERSIONED.
pkg.version
);
```
### Configure Android app startup
You may find that your project requires configuration to be setup before the JS engine has started. For example, in `expo-splash-screen` on Android, we need to specify the resize mode in the **MainActivity.java**'s `onCreate` method. Instead of attempting to dangerously regex these changes into the `MainActivity` via a dangerous mod, we use a system of lifecycle hooks and static settings to safely ensure the feature works across all supported Android languages (Java, Kotlin), versions of Expo, and combination of config plugins.
This system is made up of three components:
- `ReactActivityLifecycleListeners`: An interface exposed by `expo-modules-core` to get a native callback when the project `ReactActivity`'s `onCreate` method is invoked.
- `withStringsXml`: A mod exposed by `expo/config-plugins` which writes a property to the Android **strings.xml** file, the library can safely read the strings.xml value and do initial setup. The string XML values follow a designated format for consistency.
- `SingletonModule` (optional): An interface exposed by `expo-modules-core` to create a shared interface between native modules and `ReactActivityLifecycleListeners`.
Consider this example: We want to set a custom "value" string to a property on the Android `Activity`, directly after the `onCreate` method was invoked. We can do this safely by creating a node module `expo-custom`, implementing `expo-modules-core`, and Expo config plugins:
First, we register the `ReactActivity` listener in our Android native module, this will only be invoked if the user has `expo-modules-core` support, setup in their project (default in projects bootstrapped with Expo CLI, Create React Native App, Ignite CLI, and Expo prebuilding).
```kotlin
package expo.modules.custom
import android.content.Context
import expo.modules.core.BasePackage
import expo.modules.core.interfaces.ReactActivityLifecycleListener
class CustomPackage : BasePackage() {
override fun createReactActivityLifecycleListeners(activityContext: Context): List {
return listOf(CustomReactActivityLifecycleListener(activityContext))
}
// ...
}
```
Next we implement the `ReactActivity` listener, this is passed the `Context` and is capable of reading from the project **strings.xml** file.
```kotlin
package expo.modules.custom
import android.app.Activity
import android.content.Context
import android.os.Bundle
import android.util.Log
import expo.modules.core.interfaces.ReactActivityLifecycleListener
class CustomReactActivityLifecycleListener(activityContext: Context) : ReactActivityLifecycleListener {
override fun onCreate(activity: Activity, savedInstanceState: Bundle?) {
// Execute static tasks before the JS engine starts.
// These values are defined via config plugins.
var value = getValue(activity)
if (value != "") {
// Do something to the Activity that requires the static value...
}
}
// Naming is node module name (`expo-custom`) plus value name (`value`) using underscores as a delimiter
// i.e. `expo_custom_value`
// `@expo/vector-icons` + `iconName` -> `expo__vector_icons_icon_name`
private fun getValue(context: Context): String = context.getString(R.string.expo_custom_value).toLowerCase()
}
```
We must define default **string.xml** values which the user will overwrite locally by using the same `name` property in their **strings.xml** file.
```xml
```
At this point, bare users can configure this value by creating a string in their local **strings.xml** file (assuming they also have `expo-modules-core` support setup):
```xml
I Love Expo
```
For managed users, we can expose this functionality (safely!) via an Expo config plugin:
```js
const { AndroidConfig, withStringsXml } = require('expo/config-plugins');
function withCustom(config, value) {
return withStringsXml(config, config => {
config.modResults = setStrings(config.modResults, value);
return config;
});
}
function setStrings(strings, value) {
// Helper to add string.xml JSON items or overwrite existing items with the same name.
return AndroidConfig.Strings.setStringItem(
[
// XML represented as JSON
// value
{ $: { name: 'expo_custom_value', translatable: 'false' }, _: value },
],
strings
);
}
```
Managed Expo users can now interact with this API like so:
```json
{
"expo": {
"plugins": [["expo-custom", "I Love Expo"]]
}
}
```
By re-running `npx expo prebuild -p` (`eas build -p android`, or `npx expo run:ios`) the user can now see the changes, safely applied in their managed project!
As you can see from the example, we rely heavily on application code (expo-modules-core) to interact with application code (the native project). This ensures that our config plugins are safe and reliable, hopefully for a very long time!
## Debugging config plugins
You can debug config plugins by running `EXPO_DEBUG=1 expo prebuild`. If `EXPO_DEBUG` is enabled, the plugin stack logs will be printed, these are useful for viewing which mods ran, and in what order they ran in. To view all static plugin resolution errors, enable `EXPO_CONFIG_PLUGIN_VERBOSE_ERRORS`, this should only be needed for plugin authors. By default, some automatic plugin errors are hidden because they're usually related to versioning issues and aren't very helpful (that is, legacy package doesn't have a config plugin yet).
Running `npx expo prebuild --clean` will remove the generated native directories before compiling.
You can also run `npx expo config --type prebuild` to print the results of the plugins with the mods unevaluated (no code is generated).
Expo CLI commands can be profiled using `EXPO_PROFILE=1`.
## Introspection
Introspection is an advanced technique used to read the evaluated results of modifiers without generating any code in the project. This can be used to quickly debug the results of [static modifications](/config-plugins/development-and-debugging#static-modification) without needing to run prebuild. You can interact with introspection live, by using the [preview feature](https://github.com/expo/vscode-expo#expo-preview-modifier) of `vscode-expo`.
You can try introspection by running `expo config --type introspect` in a project.
Introspection only supports a subset of modifiers:
- `android.manifest`
- `android.gradleProperties`
- `android.strings`
- `android.colors`
- `android.colorsNight`
- `android.styles`
- `ios.infoPlist`
- `ios.entitlements`
- `ios.expoPlist`
- `ios.podfileProperties`
> Introspection only works on safe modifiers (static files like JSON, XML, plist, properties), except `ios.xcodeproj` which often requires file system changes, making it non idempotent.
Introspection works by creating custom base mods that work like the default base mods, except they don't write the `modResults` to disk at the end. Instead of persisting, they save the results to the app config under `_internal.modResults`, followed by the name of the mod such as the `ios.infoPlist` mod saves to `_internal.modResults.ios.infoPlist: {}`.
As a real-world example, introspection is used by `eas-cli` to determine what the final iOS entitlements will be in a managed app, so it can sync them with the Apple Developer Portal before building. Introspection can also be used as a handy debugging and development tool.
## Legacy plugins
To make `eas build` work the same as the classic `expo build` service, we added support for "legacy plugins" which are applied automatically to a project when they're installed in the project.
For instance, say a project has `expo-camera` installed but doesn't have `plugins: ['expo-camera']` in their **app.json**. Expo CLI would automatically add `expo-camera` to the plugins to ensure that the required camera and microphone permissions are added to the project. The user can still customize the `expo-camera` plugin by adding it to the `plugins` array manually, and the manually defined plugins will take precedence over the automatic plugins.
You can debug which plugins were added by running `expo config --type prebuild` and seeing the `_internal.pluginHistory` property.
This will show an object with all plugins that were added using `withRunOnce` plugin from `expo/config-plugins`.
Notice that `expo-location` uses `version: '11.0.0'`, and `react-native-maps` uses `version: 'UNVERSIONED'`. This means the following:
- `expo-location` and `react-native-maps` are both installed in the project.
- `expo-location` is using the plugin from the project's `node_modules/expo-location/app.plugin.js`
- The version of `react-native-maps` installed in the project doesn't have a plugin, so it's falling back on the unversioned plugin that is shipped with `expo-cli` for legacy support.
```json
{
_internal: {
pluginHistory: {
'expo-location': {
name: 'expo-location',
version: '11.0.0',
},
'react-native-maps': {
name: 'react-native-maps',
version: 'UNVERSIONED',
},
},
},
};
```
For the most _stable_ experience, you should try to have no `UNVERSIONED` plugins in your project. This is because the `UNVERSIONED` plugin may not support the native code in your project. For instance, say you have an `UNVERSIONED` Facebook plugin in your project, if the Facebook native code or plugin has a breaking change, that will break the way your project prebuilds and cause it to error on build.
## Static modification
Plugins can transform application code with regular expressions, but these modifications are dangerous if the template changes over time then the regex becomes hard to predict (similarly if the user modifies a file manually or uses a custom template). Here are some examples of files you shouldn't modify manually, and alternatives.
### Android Gradle Files
Gradle files are written in either Groovy or Kotlin. They are used to manage dependencies, versioning, and other settings in the Android app. Instead of modifying them directly with the `withProjectBuildGradle`, `withAppBuildGradle`, or `withSettingsGradle` mods, utilize the static `gradle.properties` file.
The `gradle.properties` is a static key/value pair that groovy files can read from. For example, say you wanted to control some toggle in Groovy:
```properties
expo.react.jsEngine=hermes
```
Then later in a Gradle file:
```groovy
project.ext.react = [enableHermes: findProperty('expo.react.jsEngine') ?: 'jsc']
```
- For keys in the `gradle.properties`, use camel case separated by `.`s, and usually starting with the `expo` prefix to denote that the property is managed by prebuild.
- To access the property, use one of two global methods:
- `property`: Get a property, throw an error if the property is not defined.
- `findProperty`: Get a property without throwing an error if the property is missing. This can often be used with the `?:` operator to provide a default value.
Generally, you should only interact with the Gradle file via Expo [Autolinking](/more/glossary-of-terms#autolinking), this provides a programmatic interface with the project files.
### iOS AppDelegate
Some modules may need to add delegate methods to the project AppDelegate. This can be done safely by using [AppDelegate subscribers](/modules/appdelegate-subscribers) or dangerously via the `withAppDelegate` mod (_strongly discouraged_). Using AppDelegate subscribers allows native Expo modules to react to important events in a safe and reliable way.
Below are some examples of the AppDelegate subscribers in action. Additionally, you will find many examples in community repositories on GitHub ([one such example](https://github.com/bamlab/react-native-app-security/blob/c1a861cbd348f404ec18ffae90d1c9bdc66bc00d/ios/RNASAppLifecyleDelegate.swift)).
- `expo-linking`: [**LinkingAppDelegateSubscriber.swift**](https://github.com/expo/expo/blob/b4ca25a4319d7148258ebd5121d1df40a3b1333e/packages/expo-linking/ios/LinkingAppDelegateSubscriber.swift#L14) (openURL)
- `expo-notifications`: [**NotificationsAppDelegateSubscriber.swift**](https://github.com/expo/expo/blob/bd469e421856f348d539b1b57325890147935dbc/packages/expo-notifications/ios/EXNotifications/PushToken/EXPushTokenManager.m) (didRegisterForRemoteNotificationsWithDeviceToken, didFailToRegisterForRemoteNotificationsWithError, didReceiveRemoteNotification)
### iOS CocoaPods Podfile
The **Podfile** can be customized with a regular expression (this is considered dangerous because these types of changes do not compose well and multiple changes are likely to collide), but it's more reliable to instead set configuration values in JSON file called **Podfile.properties.json**. See how `podfile_properties` is used to customize the **Podfile** below:
```ruby
require 'json'
podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'
target 'yolo27' do
use_expo_modules!
# ...
# podfile_properties['your_property']
end
```
Generally, you should only interact with the Podfile via Expo [Autolinking](/more/glossary-of-terms#autolinking), this provides a programmatic interface with the project files.
### Custom base modifiers
The Expo CLI `npx expo prebuild` command uses [`@expo/prebuild-config`](https://github.com/expo/expo/tree/main/packages/%40expo/prebuild-config) to get the default base modifiers. These defaults only manage a subset of common files, if you want to manage custom files you can do that locally by adding new base modifiers.
For example, say you wanted to add support for managing the `ios/*/AppDelegate.h` file, you could do this by adding a `ios.appDelegateHeader` modifier.
> This example uses `tsx` for simple local TypeScript support, this isn't strictly necessary. [Learn more](/guides/typescript#appconfigjs).
```ts
import { ConfigPlugin, IOSConfig, Mod, withMod, BaseMods } from 'expo/config-plugins';
import fs from 'fs';
/**
* A plugin which adds new base modifiers to the prebuild config.
*/
export function withAppDelegateHeaderBaseMod(config) {
return BaseMods.withGeneratedBaseMods<'appDelegateHeader'>(config, {
platform: 'ios',
providers: {
// Append a custom rule to supply AppDelegate header data to mods on `mods.ios.appDelegateHeader`
appDelegateHeader: BaseMods.provider({
// Get the local filepath that should be passed to the `read` method.
getFilePath({ modRequest: { projectRoot } }) {
const filePath = IOSConfig.Paths.getAppDelegateFilePath(projectRoot);
// Replace the .m with a .h
if (filePath.endsWith('.m')) {
return filePath.substr(0, filePath.lastIndexOf('.')) + '.h';
}
// Possibly a Swift project...
throw new Error(`Could not locate a valid AppDelegate.h at root: "${projectRoot}"`);
},
// Read the input file from the filesystem.
async read(filePath) {
return IOSConfig.Paths.getFileInfo(filePath);
},
// Write the resulting output to the filesystem.
async write(filePath: string, { modResults: { contents } }) {
await fs.promises.writeFile(filePath, contents);
},
}),
},
});
}
/**
* (Utility) Provides the AppDelegate header file for modification.
*/
export const withAppDelegateHeader: ConfigPlugin> = (
config,
action
) => {
return withMod(config, {
platform: 'ios',
mod: 'appDelegateHeader',
action,
});
};
// (Example) Log the contents of the modifier.
export const withSimpleAppDelegateHeaderMod = config => {
return withAppDelegateHeader(config, config => {
console.log('modify header:', config.modResults);
return config;
});
};
```
To use this new base mod, add it to the plugins array. The base mod **MUST** be added last after all other plugins that use the mod, this is because it must write the results to disk at the end of the process.
```js
// Required for external files using TS
require('tsx/cjs');
import {
withAppDelegateHeaderBaseMod,
withSimpleAppDelegateHeaderMod,
} from './withAppDelegateHeaderBaseMod.ts';
export default ({ config }) => {
if (!config.plugins) config.plugins = [];
config.plugins.push(
withSimpleAppDelegateHeaderMod,
// Base mods MUST be last
withAppDelegateHeaderBaseMod
);
return config;
};
```
For more info, see [the PR that adds support](https://github.com/expo/expo-cli/pull/3852) for this feature.
## expo install
When a node module is installed with the `npx expo install` command, if it includes a config plugin, it will be added to the project's app config automatically. This makes setup easier and helps prevent users from forgetting to add a plugin. However, this does come with a couple of caveats:
1. `npx expo install` only adds config plugins using the root **app.config.js** file automatically to the app manifest. This rule was added to prevent popular packages like `lodash` from being mistaken for a config plugin and breaking the prebuild.
2. There is currently no mechanism for detecting if a config plugin has mandatory props. Because of this, `expo install` will only add the plugin, and not attempt to add any extra props. For example, `expo-camera` has optional extra props, so `plugins: ['expo-camera']` is valid, but if it had mandatory props, then `expo-camera` would throw an error.
3. Plugins can only be automatically added when the user's project uses a static app config (**app.json** and **app.config.json**). If the user runs `expo install expo-camera` in a project with an **app.config.js**, they'll see a warning like:
```sh
Cannot automatically write to dynamic config at: app.config.js
Please add the following to your app config
{
"plugins": [
"expo-camera"
]
}
```
---
---
modificationDate: February 25, 2026
title: Using patch-project
description: Learn about how to use patch-project to create generate, apply, and preserve native changes in your Expo project.
---
# Using patch-project
Learn about how to use patch-project to create generate, apply, and preserve native changes in your Expo project.
> **Note**: `patch-project` is an alpha feature.
`patch-project` is an Expo config plugin and command-line interface (CLI) tool that generates and applies patches to preserve native changes after running `npx expo prebuild`. This tool is useful for native app developers who want to preserve customizations without needing to know how to write a config plugin, effectively generating an automatic solution that works with [Continuous Native Generation (CNG)](/workflow/continuous-native-generation).
This guide explains how to use `patch-project`, when to use it, and its limitations.
## How patch-project works
`patch-project` uses an approach to generate and automatically apply patches, which is inspired by Git. Using this command line tool requires the following steps in your project:
### Installation
To get started, you need to install the tool in your project:
```sh
npx expo install patch-project
```
This command will automatically add the `patch-project` config plugin to your [app config](/workflow/configuration):
```json
{
"expo": {
"plugins": [
"patch-project"
...
]
}
}
```
### Generate patches from existing customizations
Let's assume you manually modified native directories (**android** and **ios**) in your project. To generate patches for these native directories, you can run the following command:
```sh
npx patch-project
```
> **Note**: In scenarios where you want to generate patches for a specific platform, you can use the `--platform` option and run `npx patch-project --platform android` or `npx patch-project --platform ios`.
These patches, when generated, are saved in the **cng-patches** directory.
`.`
`app.json``with patch-project plugin`
`cng-patches`
`android+eee880ad7b07965271d2323f7057a2b4.patch``patch for android directory`
`ios+eee880ad7b07965271d2323f7057a2b4.patch``patch for ios directory`
`package.json`
`...``other project files`
Each file will be prefixed with a platform's name followed by a checksum value. For example:
```bash
ios+eee880ad7b07965271d2323f7057a2b4.patch
```
### Apply patches during prebuild
Once you have generated patches, they are automatically applied when subsequently running the `npx expo prebuild` command. The `patch-project` config plugin detects the existing patches and applies them to restore your customizations.
## When to use patch-project
You can use `patch-project` in the following scenarios:
- **Migrating existing React Native apps** that are complex because they contain extensive native customizations and re-creating these native customizations as config plugins would be time-consuming.
- **Preserving manual changes** made to **android** and/or **ios** directories while transitioning to adopt Continuous Native Generation (CNG) in your Expo project.
- **Quick prototyping** when you need to test native changes before writing config plugins.
- **Patches are applied automatically** when running the `npx expo prebuild` command subsequently. This is an advantage over tools like `patch-package` (commonly used for generating patches for npm libraries), which do not preserve and automatically apply patches during the prebuild process.
## Limitations and considerations
Patches may become invalid during Expo SDK version upgrades because:
- **Template and/or file structure changes**: The prebuild template evolves between SDK versions with new changes and file updates in native directories. This will affect the already generated diff in the **cng-patches** directory, which may no longer apply.
- **Plugin conflicts**: CNG patches can be dangerous and may break when other plugins modify the same files. For example, if you add a new plugin that updates **MainApplication.kt** and conflicts with your existing patches, the patches may no longer apply correctly. In such cases, you may need to regenerate patches.
- **iOS .pbxproj changes**: In iOS projects, applying patches to **.pbxproj** files can be fragile since this file contains UUIDs and running a command like `npx expo prebuild --clean` can change these IDs. For example, if you're adding a widget extension or making other project configuration changes, patch-based approaches may not work reliably. You can review the generated **cng-patches/ios-\*** and keep only the necessary patch. Having the patch as minimal as possible would reduce the risk of failures when applying patches.
It is recommended to regenerate patches after each SDK upgrade.
---
---
modificationDate: February 25, 2026
title: Errors and warnings
description: Learn about Redbox errors and stack traces in your Expo project.
---
# Errors and warnings
Learn about Redbox errors and stack traces in your Expo project.
When developing an application using Expo, you'll encounter a **Redbox** error or **Yellowbox** warning. These logging experiences are provided by [LogBox in React Native](https://reactnative.dev/blog/2020/07/06/version-0.63).
## Redbox error and Yellowbox warning
A Redbox error is displayed when a fatal error prevents your app from running. A Yellowbox warning is displayed to inform you that there is a possible issue and you should probably resolve it before shipping your app.
You can also create warnings and errors on your own with `console.warn("Warning message")` and `console.error("Error message")`. Another way to trigger the redbox is to throw an error and not catch it: `throw Error("Error message")`.
> This is a brief introduction to debugging a React Native app with Expo CLI. For in-depth information, see [Debugging](/debugging/runtime-issues).
## Stack traces
When you encounter an error during development, you'll see the error message and a **stack trace**, which is a report of the recent calls your application made when it crashed. This stack trace is shown both in your terminal and the Expo Go app or if you have created a development build.
This stack trace is **extremely valuable** since it gives you the location of the error's occurrence. For example, in the following image, the error comes from the file **HomeScreen.js** and is caused on line 7 in that file.
When you look at that file, on line 7, you will see that a variable called `renderDescription` is referenced. The error message describes that the variable is not found because the variable is not declared in **HomeScreen.js**. This is a typical example of how helpful error messages and stack traces can be if you take the time to decipher them.
Debugging errors is one of the most frustrating but satisfying parts of development. Remember that you're never alone. The **Expo community** and the React and React Native communities are great resources for help when you get stuck. There's a good chance someone else has run into your exact error. Make sure to read the documentation, search the [forums](https://chat.expo.dev/), [GitHub issues](https://github.com/expo/expo/issues/), and [Stack Overflow](https://stackoverflow.com/).
---
---
modificationDate: February 25, 2026
title: Debugging runtime issues
description: Learn about different techniques available to debug your Expo project.
---
# Debugging runtime issues
Learn about different techniques available to debug your Expo project.
Whether you're developing your app locally, sending it out to select beta testers, or launching your app live to the app stores, you'll always find yourself debugging issues. It's useful to split errors into two categories:
- Errors you encounter in the development
- Errors you (or your users) encounter in production
Let's go through recommended practices when dealing with each of the above situations.
> Already familiar with React Native debugging? See [Debugging tools](/debugging/tools) for Expo-specific tooling like React Native DevTools and the built-in profiler.
## Development errors
They are common errors that you encounter while developing your app. Delving into them isn't always straightforward. Usually, debugging when running your app with [Expo CLI](/more/expo-cli) is enough.
One way you can debug these issues is by looking at the [stack trace](/debugging/errors-and-warnings#stack-traces). However, in some scenarios, looking at the stack trace isn't enough as the error message traced might be a little more cryptic. For such errors, follow the steps below:
- Search for the error message in Google and [Stack Overflow](https://stackoverflow.com/questions), it's likely you're not the first person to ever run into this.
- **Isolate the code that's throwing the error**. This step is _vital_ in fixing obscure errors. To do this:
- Revert to a working version of your code. This may even be a completely blank `npx create-expo-app` project.
- Apply your recent changes piece by piece, until it breaks.
- If the code you're adding in each "piece" is complex, you may want to simplify what you're doing. For example, if you use a state management library such as Redux, you can try removing that from the equation completely to see if the issue lies in your state management (which is common in React apps).
- This should narrow down the possible sources of the error, and provide you with more information to search the internet for others who have had the same problem.
- Use breakpoints (or `console.log`s) to check and make sure a certain piece of code is being run, or that a variable has a certain value. Using `console.log` for debugging isn't considered the best practice, however, it's fast, easy, and oftentimes provides some illuminating information.
Simplifying code as much as possible to track down the source of error is a great way to debug your app and it gets exponentially easier. That's why many open-source repositories require a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) when you open an issue. It ensures you have isolated the issue and identified exactly where the problem occurs. If your app is too large and complex to do that, try and extract the functionality you're trying to add in a blank `npx create-expo-app` project, and go from there.
### Native debugging
You can perform full native debugging with Android Studio and Xcode by generating source code locally and building from that source.
#### Android Studio
Generate the native code for your project by running the following command:
```sh
npx expo prebuild -p android
```
This will add an **android** directory at the root of your project.
Open the project in Android Studio by running the command:
```sh
open -a "/Applications/Android Studio.app" ./android
```
Build the app from Android Studio and connect the debugger. See [Google's documentation](https://developer.android.com/studio/debug#startdebug) for more information.
> You can delete the **android** directory when you are done with this process. This ensures that your project remains managed by Expo CLI. Keeping the directory around and manually modifying it outside of `npx expo prebuild` means you'll need to manually upgrade and configure native libraries yourself.
#### Xcode
> This is only available for macOS users and requires Xcode to be installed.
Generate the native code for your project by running the following command:
```sh
npx expo prebuild -p ios
```
This will add an **ios** directory at the root of your project.
Open the project in Xcode by running the command which is a shortcut to open the `.xcworkspace` file from your project's **ios** directory in Xcode.
```sh
xed ios
```
Build the app with Cmd ⌘ + r or by pressing the play button in the upper left corner of Xcode.
You can now utilize [**Low-level debugger (LLDB)**](https://developer.apple.com/library/archive/documentation/IDEs/Conceptual/gdb_to_lldb_transition_guide/document/Introduction.html) and all of the other [Xcode debugging tools](https://developer.apple.com/documentation/metal/debugging_tools) to examine the native runtime.
> You can delete or gitignore the **ios** directory when you are done with this process. This ensures that your project remains managed by Expo CLI. Keeping the directory around and manually modifying it outside of `npx expo prebuild` means you'll need to manually upgrade and configure native libraries yourself.
## Viewing native logs
When your app crashes or behaves unexpectedly, the JavaScript error output doesn't always tell the full story. Native logs from Android and iOS can reveal crash reasons, native module errors, and system-level warnings that don't surface in the Metro bundler or React Native DevTools.
[How to use ADB Logcat & macOS Console to debug](https://www.youtube.com/watch?v=LvCci4Bwmpc) — In this tutorial, you'll learn how to use native device logging features like ADB Logcat and macOS Console to find bugs in your code and quickly fix them.
### Android: adb logcat
Connect your Android device (or use an emulator) and run the following command:
```sh
adb logcat
```
The Android Debug Bridge (`adb`) program is part of the Android SDK and allows you to view streaming logs. An alternative to avoid installing the Android SDK is to use [WebADB](https://webadb.com/) in Chrome.
### iOS: Console app
You can use the **Console** app in Xcode by connecting your device to your Mac (or while running an iOS Simulator). Follow the steps below to access the Console app:
Open Xcode app, and then open **Devices and Simulators** window by pressing Shift + Cmd ⌘ + 2.
If you have connected a physical device, select it under **Devices**. Otherwise, if you are using a simulator, select it under **Simulators**.
Click on **Open Console** button shown in the window to open the console app.
This will open the console app for you to view logs from your device or simulator.
## Production errors
Errors or bugs in your production app can be much harder to solve, mainly because you have less context around the error (that is, where, how, and why did the error occur?).
**The best first step in addressing a production error is to reproduce it locally.** Once you reproduce an error locally, you can follow the [development debugging process](/debugging/runtime-issues#development-errors) to isolate and address the root cause.
### Production app is crashing
When a production app crashes, there is very little information available compared to development. Start by trying to reproduce the crash locally, then work through these steps to narrow down the cause:
- **Check platform-specific crash reports.**
- For Android apps on Google Play Store, refer to the crashes section of the [Google Play Console](https://play.google.com/console/about/).
- For iOS apps on TestFlight or the App Store, use the [Crashes Organizer](https://developer.apple.com/news/?id=nra79npr) in Xcode. See also Apple's [Diagnosing Issues Using Crash Reports and Device Logs](https://developer.apple.com/documentation/xcode/diagnosing-issues-using-crash-reports-and-device-logs) guide.
- **Use native log tools.** Connect a device that reproduces the crash and use [`adb logcat` or the Console app](/debugging/runtime-issues#viewing-native-logs) to capture the native log output. Native logs often reveal the root cause when JavaScript error boundaries don't catch the problem.
- **Try production mode locally.** Running your app in **production mode** locally will show errors that normally wouldn't be thrown. To do so, you can run `npx expo start --no-dev --minify`. The `--no-dev` flag tells the server to run in production mode, and `--minify` is used to minify your code the same way it is for production JavaScript bundles.
- **Check your crash reporting dashboard.** If you use [Sentry](/guides/using-sentry), [BugSnag](/guides/using-bugsnag), or a similar service, check for the crash there first. These services provide stack traces, device info, and reproduction context.
### App crashes on certain (older) devices
This might indicate that there is a performance issue. You likely need to run your app through a profiler to get a better idea of what processes are killing the app, and [React Native provides some great documentation for this](https://reactnative.dev/docs/profiling). We also recommend using [React Native DevTools](/debugging/tools#debugging-with-react-native-devtools) and the included [profiler](/debugging/tools#profiling-javascript-performance), which makes it super easy to identify JavaScript performance sinks in your app.
### Using error reporting services
Implementing a crash and bug reporting service in your production app offers several benefits, such as:
- Real-time insights on production deployments with information to reproduce crashes and bugs.
- Setting up an alert system to get notified about fatal JavaScript errors or any other event you configure.
- Using a web dashboard to see details on exceptions such as stack traces, device information, and so on.
With Expo, you can integrate a reporting service like [Sentry](/guides/using-sentry) or [BugSnag](/guides/using-bugsnag) to get more insights in real-time.
## Stuck?
The Expo community and the React and React Native communities are great resources for help when you get stuck. There's a good chance someone else has run into the same error as you, so make sure to read the documentation, search the [forums](https://chat.expo.dev/), [GitHub issues](https://github.com/expo/expo/issues/), and [Stack Overflow](https://stackoverflow.com/).
---
---
modificationDate: February 25, 2026
title: Debugging and profiling tools
description: Learn about different tools available to inspect your Expo project at runtime.
---
# Debugging and profiling tools
Learn about different tools available to inspect your Expo project at runtime.
React Native consists of both JavaScript and native code. Making this distinction is very important when it comes to debugging. If an error is thrown from the JavaScript code, you might not find it using debugging tools for native code. This page lists a few tools to help you debug your Expo project.
## Developer menu
The **Developer menu** provides access to useful debugging functions. It is built into dev clients and Expo Go. If you are using an emulator, simulator, or have a device connected via USB, you can open this menu by pressing m in the terminal where Expo CLI has started the development server.
Alternative options to open the Developer menu
- Android device (without USB): Shake the device vertically.
- Android Emulator or device (with USB):
- Press Cmd ⌘ + m or Ctrl + m.
- Run the following command in the terminal to simulate pressing the menu button:
```sh
adb shell input keyevent 82
```
- iOS device (without USB):
- Shake the device.
- Touch three fingers to the screen.
- iOS Simulator or device (with USB):
- Press Ctrl + Cmd ⌘ + z or Cmd ⌘ + d
Once the Developer menu is open, it will appear as below:
The Developer menu provides the following options:
- **Copy link**: To copy the dev server address in dev client or [`exp://`](/linking/into-your-app#test-a-link-using-expo-go) link in Expo of your app.
- **Reload**: To reload you app. Usually, not necessary since Fast Refresh is enabled by default.
- **Go Home**: To leave your app and navigate back to the dev client's or Expo Go app's Home screen.
- **Toggle performance monitor**: To view the performance information about your app.
- **Toggle element inspector**: To enable or disable the element inspector overlay.
- **Open DevTools** (formerly **Open JS debugger**): To open React Native DevTools which provides access to Console, Sources, Network (**Expo only**), Memory, Components, and Profiler, tabs for apps using Hermes. For more information, see the [Debugging with React Native DevTools](/debugging/tools#debugging-with-react-native-devtools) section.
- **Fast Refresh**: To toggle automatic refreshing of the JS bundle whenever you make changes to files in your project using a text editor.
Now, let's explore some of these options in details.
### Toggle performance monitor
Opens up a small overlay that provides the following performance information about your app:
- RAM usage of a project.
- JavaScript heap (this is an easy way to know of any memory leaks in your application).
- Two Views. The top indicates the number of views for the screen and the bottom indicates the number of views in the component.
- Frames Per Second for the UI and JS threads. The UI thread is used for native Android or iOS UI rendering. The JS thread is where most of your logic runs, including API calls, touch events, and so on.
### Toggle element inspector
Opens up the element inspector overlay:
This overlay has capabilities to:
- Inspect: Inspect elements
- Perf: Show Performance overlay
- Network: Show network details
- Touchables: Highlight touchable elements
## Debugging with React Native DevTools
> **Starting from React Native 0.76**, React Native DevTools has replaced Chrome DevTools.
**React Native DevTools** is a modern debugging tool for Expo and React Native apps. It allows you to gain insights into the JavaScript code of your app by accessing the [Console](/debugging/tools#interacting-with-the-console), [Sources](/debugging/tools#pausing-on-breakpoints), [Network](/debugging/tools#inspecting-network-requests-expo-only) (**Expo only**), and [Memory](/debugging/tools#inspecting-memory) tabs. It also has **built-in support for React DevTools** such as [Components](/debugging/tools#inspecting-components) and [Profiler](/debugging/tools#profiling-javascript-performance) tabs. All of these inspectors can be accessed using [dev clients](/more/glossary-of-terms#dev-clients) or Expo Go.
You can use the React Native DevTools on any app using [Hermes](/guides/using-hermes). **To open it, start your app and press j in the terminal where Expo was started**. Once you have opened the React Native DevTools, it will appear as below:
### Pausing on breakpoints
You can pause your app on specific parts of your code. To do this, set the breakpoint under the Sources tab by clicking the line number or add the `debugger` statement in your code.
Once your app is executing code that has a breakpoint, it will entirely pause your app. This allows you to inspect all variables and functions in that scope. You can also execute code in the [Console](/debugging/tools#interacting-with-the-console) tab as part of your app.
### Pausing on exceptions
If your app throws unexpected errors, it can be hard to find the source of the error. You can use React Native DevTools to pause your app and inspect the stack trace and variables the moment it throws an error.
> Some errors might be caught by other components in your app, such as Expo Router. In these cases, you can turn on **Pause on caught exceptions**. It will enable you to inspect any thrown error, even when handled properly.
### Interacting with the console
The **Console** tab gives you access to an interactive terminal, connected directly to your app. You can write any JavaScript inside this terminal to execute snippets of code as if it were part of your app. The code is executed in the global scope by default. But, when using breakpoints from the [Sources](/debugging/tools#pausing-on-breakpoints) tab, it executes in the scope of the reached breakpoint. This allows you to invoke methods and access variables throughout your app.
### Inspecting network requests (Expo only)
> The Network tab in React Native DevTools is only available when you have `expo` installed in your project.
The **Network** tab gives you insights into the network requests made by your app. You can inspect each request and response by clicking on them. This includes `fetch` requests, external loaded media, and in some cases, even requests made by native modules.
> See the [Inspecting network traffic](/debugging/tools#inspecting-network-traffic) for alternative ways to inspect network requests.
### Inspecting memory
The **Memory** tab allows you to inspect the memory usage and take a heap snapshot of your app's JavaScript code.
### Inspecting components
The **Components** tab allows you to inspect the React components in your app. You can view the props, and styles of each component by hovering that component in React Native DevTools. This is a great way to debug your app's UI and understand how your components are structured.
### Profiling JavaScript performance
> Profiles are not yet symbolicated with sourcemaps, and [can only be used in debug builds](https://github.com/facebook/hermes/issues/760). These limitations will be addressed in upcoming releases.
The **Profiler** tab allows you to record and analyze the performance of your app's JavaScript. You can start recording, interact with your app, and stop recording to analyze the profile.
> To profile the native runtime, use the tools included in Android Studio or Xcode.
## Debugging with VS Code
> VS Code debugger integration is in alpha. For the most stable debugging experience, [use the React Native DevTools](/debugging/tools#debugging-with-react-native-devtools).
VS Code is a popular code editor, which has a built-in debugger. This debugger uses the same system as the React Native DevTools — the inspector protocol.
You can use this debugger with the [Expo Tools](https://github.com/expo/vscode-expo#readme) VS Code extension. This debugger allows you to set breakpoints, inspect variables, and execute code through the debug console.
To start debugging:
- Connect your app
- Open VS Code command palette (based on your computer, it's either Ctrl + Shift + p or Cmd ⌘ + Shift + p)
- Run the **Expo: Debug ...** VS Code command.
This will attach VS Code to your running app.
Alternatively, if you want a fully-featured IDE setup in VS Code, you might want to check out the [Radon IDE](https://ide.swmansion.com/) extension (paid with a 30-day free trial). It turns your editor into a powerful environment designed specifically for React Native and Expo projects, with advanced debugging, a network inspector, router integration, and other built-in tools.
## React Native Debugger
> The React Native Debugger requires Remote JS debugging, which has been deprecated since [React Native 0.73](https://reactnative.dev/docs/other-debugging-methods#remote-javascript-debugging-deprecated).
The React Native Debugger is a standalone app that wraps the React DevTools, Redux DevTools, and React Native DevTools. Unfortunately, it requires the [deprecated Remote JS debugging workflow](https://github.com/jhen0409/react-native-debugger/discussions/774) and is incompatible with Hermes.
If you are using Expo **SDK 50** or **above**, you can use the [Expo dev tools plugins](/debugging/devtools-plugins) equivalents to the React Native Debugger:
- [React Native DevTools](/debugging/tools#debugging-with-react-native-devtools)
- [Redux DevTools](/debugging/devtools-plugins#redux)
If you are using Expo SDK 49 and earlier, you can use the React Native Debugger. This section provides quick get started instructions. For in-depth information, check its [documentation](https://github.com/jhen0409/react-native-debugger#documentation).
You can install it via the [release page](https://github.com/jhen0409/react-native-debugger/releases), or if you're on macOS you can run:
```sh
brew install react-native-debugger
```
### Startup
After firing up React Native Debugger, you'll need to specify the port (shortcuts: Cmd ⌘ + t on macOS, Ctrl + t on Linux/Windows) to `8081`. After that, run your project with `npx expo start`, and select `Debug remote JS` from the Developer Menu. The debugger should automatically connect.
In the debugger console, you can see the Element tree, as well as the props, state, and children of whatever element you select. You also have the Chrome console on the right, and if you type `$r` in the console, you will see the breakdown of your selected element.
If you right-click anywhere in the React Native Debugger, you'll get some handy shortcuts to reload your JS, enable/disable the element inspector, network inspector, and to log and clear your `AsyncStorage` content.
### Inspecting network traffic
It's easy to use the React Native Debugger to debug your network request: right-click anywhere in the React Native Debugger and select `Enable Network Inspect`. This will enable the Network tab and allow you to inspect requests of `fetch` and `XMLHttpRequest`.
There are however [some limitations](https://github.com/jhen0409/react-native-debugger/blob/master/docs/network-inspect-of-chrome-devtools.md#limitations), so there are a few other alternatives, all of which require using a proxy:
- [Charles Proxy](https://www.charlesproxy.com/documentation/configuration/browser-and-system-configuration/) (~$50 USD, our preferred tool)
- [Proxyman](https://proxyman.io) (Free version available or $49 to $59 USD)
- [mitmproxy](https://medium.com/@rotxed/how-to-debug-http-s-traffic-on-android-7fbe5d2a34#.hnhanhyoz)
- [Fiddler](http://www.telerik.com/fiddler)
## Debugging production apps
In reality, apps often ship with bugs. Implementing a crash and bug reporting system can help you get real-time insights of your production apps. See [Using error reporting services](/debugging/runtime-issues#using-error-reporting-services) for more details.
---
---
modificationDate: February 25, 2026
title: Dev tools plugins
description: Learn about using dev tools plugins to inspect and debug your Expo project.
---
# Dev tools plugins
Learn about using dev tools plugins to inspect and debug your Expo project.
Dev tools plugins are available in your local development environment to help you debug your app. They consist of a small amount of code you add to a project that enables two-way communication between the app and an external Chrome window. This setup provides display tools to inspect the app, trigger certain behaviors for testing, and more.
Dev tools plugins are similar to Flipper plugins that are available in Development builds and Expo Go, and do not require adding native modules or config plugins to your project.
## Add a dev tools plugin to a project
To add a dev tool plugin to your app, install it as a package and add a small snippet to connect the code to your app. This code is invoked from the app's root component to establish a two-way communication between your app and the plugin. Then, the plugin can inspect aspects of your app for the entire time your app runs in development mode.
All [Expo dev tools plugins](/debugging/devtools-plugins#expo-dev-tools-plugins) and plugins created with [our creation tool](/debugging/create-devtools-plugins) export a hook that you can use to connect the plugin to your app. The hook and any functions returned from it will no-op when the app is not running in development mode.
Some plugin hooks require parameters that relate to how the plugin inspects your app. For instance, a plugin for inspecting the React Navigation state might require a reference to the navigation root.
To start using the plugin, use the hook in your app's root component:
```jsx
import { useMyDevToolsPlugin } from 'my-devtools-plugin';
export default App() {
useMyDevToolsPlugin();
return (/* rest of your app */)
}
```
In some cases, you may need to interact with a plugin directly. All plugins communicate through exports from `expo/devtools`, and you can send and listen to messages through `useDevToolsPluginClient`. Be sure to pass the same plugin name to `useDevToolsPluginClient` as is used by the plugin's web user interface:
```jsx
import { useDevToolsPluginClient } from 'expo/devtools';
export default App() {
const client = useDevToolsPluginClient('my-devtools-plugin');
useEffect(() => {
// receive messages
client?.addMessageListener("ping", (data) => {
alert(`Received ping from ${data.from}`);
});
// send messages
client?.sendMessage("ping", { from: "app" });
}, []);
return (/* rest of your app */)
}
```
### Compatibility with Expo Go and Development builds
Dev tools plugins should only include JavaScript code. They are generally compatible with Expo Go and [Development builds](/develop/development-builds/introduction) and should not require creating a new development build to add the plugin. If a package's underlying module that the plugin inspects includes native code and is not part of Expo Go, create a new development build to use both the component and the plugin from that package.
For example, a dev tools plugin that inspects [React Native Firebase](/guides/using-firebase#using-react-native-firebase) will not work with Expo Go. React Native Firebase includes native code that is not part of Expo Go. To use the dev tools plugin and React Native Firebase, create a development build.
## Using a dev tools plugin
After installing the dev tools plugin and adding the connecting required code to your project, you can start the dev server up with `npx expo start`. Then press shift + m to open the list of available dev tools plugins. Select the plugin you want to use, and it will open in a new Chrome window.
> When starting the dev server with the Expo CLI, there is an option to press ? to **show all commands**. This shows additional commands, including the shortcut to open **more tools**. Dev tools plugins can also be selected in this menu.
## Expo dev tools plugins
Expo provides some dev tools plugins for common debugging tasks. Follow the instructions below to start using them in your app.
> **Note**: Each of the following dev tools plugin hooks will only enable the plugin in development mode. It doesn't affect your production bundle.
### React Navigation
Inspired by [`@react-navigation/devtools`](https://github.com/react-navigation/react-navigation/tree/main/packages/devtools), the React Navigation dev tools plugin allows seeing the history of [React Navigation](https://reactnavigation.org/) actions and state. You can also rewind to previous points in your navigation history and send deep links to your app. Since Expo Router is built upon React Navigation, this plugin is fully compatible with [Expo Router](/router/introduction).
To use the plugin, start by installing the package:
```sh
npx expo install @dev-plugins/react-navigation
```
Pass the navigation root to the plugin in your app's entry point:
```jsx
import { useEffect, useRef } from 'react';
import { useNavigationContainerRef, Slot } from 'expo-router';
import { useReactNavigationDevTools } from '@dev-plugins/react-navigation';
export default Layout() {
const navigationRef = useNavigationContainerRef();
useReactNavigationDevTools(navigationRef);
return ;
}
```
In the terminal, run `npx expo start`, press shift + m to open the list of dev tools, and then select the React Navigation plugin. This will open the plugin's web interface, showing your navigation history as you navigate through your app.
### Apollo Client
Inspired by [`react-native-apollo-devtools`](https://github.com/razorpay/react-native-apollo-devtools), the Apollo Client dev tools plugin allows inspecting cache, query, and mutation for the Apollo Client.
To use the plugin, start by installing the package:
```sh
npx expo install @dev-plugins/apollo-client
```
Then pass your client instance to the plugin in your app's root component or where you wrap the rest of your app in the `ApolloProvider`:
```jsx
import { ApolloProvider, ApolloClient, InMemoryCache } from '@apollo/client';
import { useApolloClientDevTools } from '@dev-plugins/apollo-client';
const client = new ApolloClient({
uri: 'https://demo.test.com/',
cache: new InMemoryCache(),
});
export default function App() {
useApolloClientDevTools(client);
return {/* ... */};
}
```
In the terminal, run `npx expo start`, press shift + m to open the list of dev tools, and then select the Apollo Client plugin. This will open the plugin's web interface, showing your query history, cache, and mutations as your app performs Apollo Client operations.
### React Query
Inspired by [`react-query-native-devtools`](https://github.com/bgaleotti/react-query-native-devtools), the React Query dev tools plugin lets you explore data and queries, cache status, and refetch and remove queries from the cache from [TanStack Query](https://tanstack.com/query/latest/).
To use the plugin, start by installing the package:
```sh
npx expo install @dev-plugins/react-query
```
Then pass your client instance to the plugin in your app's root component or where you wrap the rest of your app in the `QueryClientProvider`:
```jsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useReactQueryDevTools } from '@dev-plugins/react-query';
const queryClient = new QueryClient({});
export default function App() {
useReactQueryDevTools(queryClient);
return {/* ... */};
}
```
In the terminal, run `npx expo start`, press shift + m to open the list of dev tools, and then select the React Query plugin. This will open the plugin's web interface, displaying queries as they are used in your app.
### Redux
The `redux-devtools-expo-dev-plugin` is based on the [Redux DevTools](https://github.com/reduxjs/redux-devtools/) (from the Chrome extension). It provides a live list of actions and how they affect the state, and the ability to rewind, replay, and dispatch actions from the DevTools.
To use the plugin, start by installing the package:
```sh
npx expo install redux-devtools-expo-dev-plugin
```
If you're using `@reduxjs/toolkit`, modify the `configureStore` call to disable the built-in dev tools by passing in `devTools: false`. Then, add in the Expo DevTools plugin enhancer by concatenating the `devToolsEnhancer()`. The `configureStore` call is going to look like the following:
```js
import devToolsEnhancer from 'redux-devtools-expo-dev-plugin';
const store = configureStore({
reducer: rootReducer,
devTools: false,
enhancers: getDefaultEnhancers => getDefaultEnhancers().concat(devToolsEnhancer()),
});
```
In the terminal, run `npx expo start`, press shift + m to open the list of dev tools, and then select `redux-devtools-expo-dev-plugin`. This will open the plugin's web interface, displaying the actions and contents of your store as actions are dispatched.
For complete installation and usage instructions, including if you're using `redux` directly rather than `@reduxjs/toolkit`, [see the project's README](https://github.com/matt-oakes/redux-devtools-expo-dev-plugin).
### TinyBase
The TinyBase dev tools plugin connects the TinyBase Store Inspector to your app, allowing you to view and update the contents of your app's store.
To use the plugin, start by installing the package:
```sh
npx expo install @dev-plugins/tinybase
```
Then pass your client instance to the plugin in your app's root component or where you wrap the rest of your app with the store's `Provider`:
```jsx
import { createStore } from 'tinybase';
import { useValue, Provider } from 'tinybase/lib/ui-react';
import { useTinyBaseDevTools } from '@dev-plugins/tinybase';
const store = createStore().setValue('counter', 0);
export default function App() {
useTinyBaseDevTools(store);
return {/* ... */};
}
```
In the terminal, run `npx expo start`, press shift + m to open the list of dev tools, and then select the Tinybase plugin. This will open the plugin's web interface, displaying the contents of your store as it is modified.
---
---
modificationDate: February 25, 2026
title: Create a dev tools plugin
description: Learn how to create a dev tools plugin to enhance your development experience.
---
# Create a dev tools plugin
Learn how to create a dev tools plugin to enhance your development experience.
> **Tip:** Check out the [Expo DevTools Plugins](https://github.com/expo/dev-plugins) for complete examples.
You can create a dev tools plugin, whether that's for inspecting aspects of a common framework or library or something specific to your custom code. This guide will walk you through creating a dev tools plugin.
## What is a dev tools plugin?
A dev tools plugin runs in your web browser in your local development environment and connects to your Expo app.
A plugin consists of three key elements:
- An Expo app to display the dev tools web user interface.
- An **expo-module.config.json** for Expo CLI recognition.
- Calls to `expo/devtools` API for the app to communicate back and forth with the dev tool's web interface.
Plugins can be distributed on npm or included inside your app's monorepo. They typically export a single hook that can be used in your app's root component to initiate two-way communication with the web interface when your app is running in debug mode.
## Create a plugin
### Create a new plugin project
`create-dev-plugin` will set up a new plugin project for you. Run the following command to create a new plugin project:
```sh
npx create-dev-plugin@latest
```
`create-dev-plugin` will prompt you for the name of your plugin, a description, and the name of the hook that will be used by consumers of your plugin.
The plugin project will contain the following directories:
- **src** - this exports the hook that will be used inside the consuming app to connect it to the plugin.
- **webui** - this contains the web user interface for the plugin.
### Customize a plugin's functionality
The template includes a simple example of sending and receiving messages between the plugin and the app. `useDevToolsPluginClient`, imported from `expo/devtools`, provides functionality for sending and receiving messages between the plugin and the app.
The client object returned by `useDevToolsPluginClient` includes:
### `addMessageListener`
Listens for a message matching the typed string and invokes the callback with the message data.
```jsx
const client = useDevToolsPluginClient('my-devtools-plugin');
client.addMessageListener('ping', data => {
alert(`Received ping from ${data.from}`);
});
```
### `sendMessage`
Listens for a message matching the typed string and invokes the callback with the message data.
```jsx
const client = useDevToolsPluginClient('my-devtools-plugin');
client?.sendMessage('ping', { from: 'web' });
```
Edit the Expo app inside the **webui** directory to customize the user interface that displays diagnostic information from your app or triggers test scenarios:
```tsx
import { useDevToolsPluginClient, type EventSubscription } from 'expo/devtools';
import { useEffect } from 'react';
export default function App() {
const client = useDevToolsPluginClient('my-devtools-plugin');
useEffect(() => {
const subscriptions: EventSubscription[] = [];
subscriptions.push(
client?.addMessageListener('ping', data => {
alert(`Received ping from ${data.from}`);
})
);
return () => {
for (const subscription of subscriptions) {
subscription?.remove();
}
};
}, [client]);
}
```
Edit the hook in the **src** directory to customize what diagnostic information is sent to the plugin or how the app should respond to any messages from the web user interface:
```tsx
import { useDevToolsPluginClient } from 'expo/devtools';
export function useMyDevToolsPlugin() {
const client = useDevToolsPluginClient('my-devtools-plugin');
const sendPing = () => {
client?.sendMessage('ping', { from: 'app' });
};
return {
sendPing,
};
}
```
If you update the hook to return functions that will be called by the app, you will also need to update **src/index.ts** so it exports no-op functions when the app is not running in debug mode:
```diff
if (process.env.NODE_ENV !== 'production') {
useMyDevToolsPlugin = require('./useMyDevToolsPlugin').useMyDevToolsPlugin;
} else {
useMyDevToolsPlugin = () => ({
+ sendPing: () => {},
});
}
```
## Test a plugin
Since the plugin web UI is an Expo app, you can test it just like you would any other Expo app, with `npx expo start`, except that you will run it in the browser only. The template includes a convenience command to run the plugin in local development mode:
```sh
npm run web:dev
```
## Build a plugin for distribution
To prepare your plugin for distribution or use within your monorepo, you will need to build the plugin with the following command:
```sh
npm run build:all
```
This command will build the hook code into the **build** directory, and the web user interface into the **dist** directory.
## Use the plugin
Import the plugin's hook into your app's root component and call it to connect your app to the plugin:
```jsx
import { useMyDevToolsPlugin } from 'my-devtools-plugin';
import { Button } from 'react-native';
export default function App() {
const { sendPing } = useMyDevToolsPlugin();
return (
);
}
```
---
---
modificationDate: February 25, 2026
title: Databases in Expo and React Native apps
description: Learn about adding a database to your Expo project.
---
# Databases in Expo and React Native apps
Learn about adding a database to your Expo project.
Most apps need to persist data beyond the lifetime of a single session. You can use a cloud-hosted database to store your app's data and sync it across devices and users.
## Convex
[Convex](https://www.convex.dev/) is a TypeScript-based database that requires no cluster management, SQL, or ORMs. Convex provides real-time updates over a WebSocket, making it perfect for reactive apps.
[Using Convex](/guides/using-convex) — Add a database to your app with Convex.
## Supabase
[Supabase](https://supabase.com/) is an app development platform that provides hosted backend services such as a Postgres database, user authentication, file storage, edge functions, realtime syncing, and a vector and AI toolkit.
[Using Supabase](/guides/using-supabase) — Add a Postgres database and user authentication to your app with Supabase.
## Firebase
[Firebase](https://firebase.google.com/) is an app development platform that provides hosted backend services such as real-time database, cloud storage, authentication, crash reporting, analytics, and more. It is built on Google's infrastructure and scales automatically.
[Using Firebase](/guides/using-firebase) — Get started with Firebase JS SDK and React Native Firebase.
---
---
modificationDate: February 25, 2026
title: Authentication in Expo and React Native apps
description: Learn about setting up authentication in your Expo project.
---
# Authentication in Expo and React Native apps
Learn about setting up authentication in your Expo project.
Authentication is a critical part of 90 to 95 percent of modern apps. This guide explains common methods, patterns, and solutions to help you implement authentication in your Expo app.
> **TL;DR**: Auth is hard. If you want to skip the complexity, jump to the [Auth solutions](/develop/authentication#auth-solutions) section for ready-made solutions. Otherwise, keep reading.
Implementing authentication involves more than writing client-side code. You'll need to manage server requests, password flows, third-party providers like Google or Apple, email handling, and OAuth standards. It can get complex quickly.
There are several types of authentication methods. Some are simple and effective, while others offer a better user experience but require more work. Let's look at the most common approaches and how you can implement them.
## Navigation auth flow
Let's start with the basics: any authentication system needs to separate **public screens** (such as login or signup) from **protected screens** (such as home or profile). At the navigation level, it comes down to a simple check: is the user authenticated?
To begin, you can simulate this using a hardcoded boolean value, like `isAuthenticated = true`, and build your navigation logic around it. Once everything is working, you can plug in your real authentication flow.
Using Expo Router
Expo Router v5 introduced [protected routes](/router/advanced/protected), which prevent users from accessing certain screens unless they are authenticated. This feature works well for client-side navigation and simplifies your setup.
If you're using an older version of Expo Router, you can use [redirects](/router/advanced/authentication-rewrites) instead. Redirects provide the same result but require a bit more manual configuration. They are still supported in Expo Router v5 for backward compatibility.
[Expo Router Protected Routes](https://www.youtube.com/watch?v=zHZjJDTTHJg) — Learn how to implement an authentication flow with Expo Router
Using React Navigation
If you're using React Navigation, they offer a helpful [authentication flow guide](https://reactnavigation.org/docs/auth-flow/) that explains how to structure your navigation logic. It includes examples for both [static](https://reactnavigation.org/docs/auth-flow/?config=static#how-it-will-work) and [dynamic](https://reactnavigation.org/docs/auth-flow/?config=dynamic#how-it-will-work) approaches based on the user's authentication state.
Both Expo Router and React Navigation give you flexible tools to implement protected navigation based on whether the user is logged in.
## Email and password
Email and password is a popular option when adding authentication to your app.
To make this flow user-friendly, you also need to implement forgot password and reset password functionality so users who lose access to their accounts can recover them.
If you want a quicker solution, several services offer built-in email and password authentication, including [Clerk](/develop/authentication#clerk), [Supabase](/develop/authentication#supabase), [Cognito](/develop/authentication#cognito), [Firebase](/develop/authentication#firebase-auth), and [Better Auth](/develop/authentication#better-auth). Most of these have generous free tiers, but it is a good idea to evaluate pricing if your app grows quickly.
The biggest advantage of these services is their ease of integration. They usually offer clear documentation, starter kits, and prebuilt components that save you time.
Security checklist (OWASP) and store-review gotchas
If you're building this flow yourself, be sure to review the [Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#authentication-cheat-sheet) by OWASP. It outlines best practices for password length, encryption, recovery, secure storage, and more.
> Adding email and password authentication is usually enough to pass App Store and Play Store review. You can submit your app with this method first. If you include "Sign in with Google," Apple may reject your app unless you also support "Sign in with Apple." The same rule applies in reverse on Google Play.
[Better Auth Example](https://github.com/expo/examples/tree/master/with-better-auth) — An example demonstrating email and password authentication with Better Auth.
## Passwordless login
Passwordless login removes the need for users to create or remember a password. Instead, they provide their email address or phone number during registration. Your app then sends a [magic link](https://auth0.com/docs/authenticate/passwordless/authentication-methods/email-magic-link#classic-login-flow-with-magic-links) or [one-time passcode (OTP)](https://en.wikipedia.org/wiki/One-time_password) to their inbox or device. This is a smoother experience for most users and reduces friction during onboarding.
Magic Links
With magic links, the user receives an email containing a link that redirects them back into your app. If everything works correctly, the session is verified and established.
A key detail here is [deep linking](/linking/into-your-app). Since users leave the app to check their email, the link must open your app and route them to the correct screen. If deep linking fails, the session cannot be validated, and the login flow breaks.
If you're using Expo Router deep linking is handled automatically (for most cases). You usually don't need to configure anything extra to make magic links work properly, which makes this approach even easier to adopt. See [Linking into your app](/linking/into-your-app) to learn more.
[React Navigation](https://reactnavigation.org/) also supports deep linking, but you will need to configure it manually. See its [Deep Linking guide](https://reactnavigation.org/docs/deep-linking/) for more details.
One-Time Passcodes (OTP)
An alternative to magic links is sending a one-time passcode by email or SMS. Instead of clicking a link, the user copies the code and manually returns to the app to enter it. This must happen within a specific time window before the code expires.
There's no deep linking involved here. The user stays in control of the flow and must return to the app themselves.
Fortunately, newer versions of Android and iOS automatically detect passcodes in incoming messages. This enables autofill suggestions above the keyboard, allowing users to enter the code with a single tap. When this works, the experience is seamless.
> Magic links and passcodes are both valid authentication methods for Google Play Store and Apple App Store reviews. You can submit your app with either of these methods as the only option and get approved, even before adding social or OAuth login options.
## OAuth 2.0
To let your users log in using their existing accounts from services like Google, Apple, GitHub, and more, you can use OAuth 2.0.
[OAuth 2.0](https://oauth.net/2) is a widely used, secure protocol that allows your app to access user information from another service, without needing to handle passwords. It allows your users to log in with a single tap, which saves time, builds trust, and removes the need to manage passwords.
> OAuth flows can be complex. If you're looking for a simple integration, most providers offer SDKs and services that handle everything for you. You can learn more about these in the [Auth solutions](/develop/authentication#auth-solutions) section.
If you are looking for full control or want to understand how OAuth works under the hood, the following sections show how to implement a complete OAuth flow yourself using Expo.
### How OAuth works
OAuth works by introducing an authorization server that acts as a secure middleman. Instead of giving your app their password, users log in through this server and approve access to specific data (like their name or email). The server then issues a temporary code, which your app can exchange for a secure access token.
In this diagram, 'client' simply refers to the application and does not imply any specific implementation details, such as whether it runs on a server, desktop, mobile device, or other platform.
Once you understand this pattern, you can apply it to any provider. The setup for Google, Apple, or GitHub will follow the same general steps.
### Custom OAuth with Expo API Routes
The previous diagram shows a high-level overview of the OAuth flow. However, the preferred method for a client to obtain an authorization grant from the user is to use an authorization server as an intermediary, which is exactly what you can build using Expo API Routes.
The following diagram illustrates this flow in more detail:
Expo lets you implement the entire OAuth flow directly in your app using:
[Expo Router](/router/introduction)
[Expo Router API Routes](/router/web/api-routes)
[Expo AuthSession](/versions/latest/sdk/auth-session)
Some providers offer native APIs to handle the sign-in flow directly within the app. Google offers a native Sign in with Google experience on Android. If you're looking for a native implementation, see the [Google authentication guide](/guides/google-authentication). Apple provides Sign in with Apple, which uses a native bottom sheet and Face ID on iOS. See [`expo-apple-authentication`](/versions/latest/sdk/apple-authentication) reference.
The following setup gives you full control over the login experience across Android, iOS, and web.
What are Expo API Routes?
[Expo Router API Routes](/router/web/api-routes) allow you to write server-side logic directly inside your Expo app. You can define functions that handle requests just like an Express or Next.js backend, no need for an external server.
This makes it easy to securely handle sensitive parts of the auth flow, like the [authorization code exchange](https://www.oauth.com/oauth2-servers/pkce/authorization-code-exchange), directly within your app. Since these routes run on the server, you can safely manage secrets, issue JWTs, and validate tokens.
> You're essentially building a lightweight custom auth server scoped to your own application, all using your Expo project.
What is Expo AuthSession?
[Expo AuthSession](/versions/latest/sdk/auth-session) is a client-side package that helps you open a web browser or native modal to start the OAuth login flow. It handles redirection, parses the authorization response, and brings the user back into your app.
It's the tool that kicks off the flow and talks to your API Route after the user authorizes access. See [Authentication with OAuth or OpenID providers](/guides/authentication) for more information.
This setup lets you:
- Start the login flow using AuthSession
- Receive the auth code in your API Route
- Exchange the code for a token securely
- Generate a custom JWT with your own logic
- Return that token to the client
- Store sessions using cookies (Web) or JWTs (Native)
- Deploy instantly using EAS Hosting (free to start)
The following tutorials cover implementing OAuth on Android, iOS, and web, including how to create and verify custom JWTs, manage sessions, and protect API routes. If you're new to this flow, we recommend starting with the Google tutorial.
[Google Sign-In with Expo OAuth](https://www.youtube.com/watch?v=V2YdhR1hVNw) — Learn how to implement Google Sign-In with Expo Router API Routes
[Sign in with Apple using Expo](https://www.youtube.com/watch?v=tqxTijhYhp8) — Learn how to implement Sign in with Apple
Managing sessions after OAuth
Handling the OAuth flow securely is just the beginning. Once the user is authenticated, you need to think about how to store, restore, and validate their session.
This includes:
- Storing the session securely on the client
- Restoring it when the app restarts
- Protecting your API routes so only authenticated users can access them
Traditionally, [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#what_cookies_are_used_for) are used to store sessions on the web, while [JSON Web Tokens (JWTs)](https://en.wikipedia.org/wiki/JSON_Web_Token) are common in native applications.
The above tutorials demonstrate exactly how to handle this. After receiving the ID token from a provider like Google or Apple, you generate a custom JWT on the server using Expo API Routes.
This gives you full control over the session, including:
- Structuring the payload using consistent fields across providers
- Customizing expiration times
- Signing the token with a secret key so your server can verify it later
Once the token is created:
- For Android and iOS apps, you can store it securely using [`expo-secure-store`](/versions/latest/sdk/securestore)
- For web apps, you can set it as a secure cookie to maintain the session
On every request, the token is sent back to your server, where you verify the signature and check the expiration. If everything checks out, you continue processing the request.
This session model keeps your backend stateless, scalable, and secure, and works consistently across platforms.
All of this is covered in the video tutorials linked above, including:
- Generating and verifying custom JWTs
- Handling session storage with Secure Store and cookies
- Protecting API routes with authentication logic
## Auth solutions
If you prefer not to build a full authentication system from scratch, several services offer built-in solutions with first-class support for Expo. Here are some of the most popular options:
Better Auth
[BetterAuth](https://www.better-auth.com/docs/integrations/expo) is a modern, open-source authentication provider built for developers. It integrates smoothly with Expo, and they offer a guide that shows how to use it with [Expo API Routes](https://www.better-auth.com/docs/integrations/expo) for full control. It works well with any provider and deploys easily with EAS Hosting.
Clerk
[Clerk](https://clerk.com/expo-authentication) is a powerful, full-featured authentication service with excellent Expo support. It includes email/password, passcodes, magic links, OAuth providers, and even passkeys. They also offer a native Expo module that handles much of the integration for you.
Supabase
[Supabase](https://supabase.com/docs/guides/getting-started/tutorials/with-expo-react-native) provides a full backend platform, including a built-in authentication service that works with any OAuth provider. It integrates well with Expo apps and also includes support for email, magic links, and more.
Cognito
[AWS Cognito](https://medium.com/@juliuscecilia33/aws-cognito-and-react-native-bf23ef7fea23) is Amazon's solution for managing user pools and identity. It connects seamlessly with other AWS services and can be integrated into Expo apps using AWS Amplify. It does require more configuration, but it's robust and scalable.
Firebase Auth
[Firebase Authentication](https://rnfirebase.io/auth/usage) is Google's auth platform and supports email, magic links, and OAuth providers. It works with React Native through [`react-native-firebase`](https://github.com/invertase/react-native-firebase), which is compatible with Expo development builds.
## Modern methods
Once you have a working authentication system in place, you can improve the user experience by adding optional but powerful enhancements like biometrics and passkeys. These features add convenience, trust, and speed to your login flows.
Biometrics
Biometrics like Face ID and Touch ID can be used to unlock the app or confirm identity after a valid session is established. These are not authentication methods on their own, but act as a local gate that makes re-authentication faster and more secure.
React Native provides access to biometric APIs through libraries like [`expo-local-authentication`](/versions/latest/sdk/local-authentication) or [`react-native-biometrics`](https://github.com/SelfLender/react-native-biometrics).
Passkeys
[Passkeys](https://safety.google/authentication/passkey) are a new, passwordless way to log in to apps and websites. Backed by Apple, Google, and Microsoft, they use platform-level cryptography and biometrics to authenticate users without passwords.
Passkeys offer a seamless and secure experience, but they require a user to already be authenticated before registering one. They also require extra configuration if you're not using a provider that handles them for you.
- React Native passkey support: [`react-native-passkeys`](https://github.com/peterferguson/react-native-passkeys)
- Native passkey support with Clerk: [Clerk Passkeys for Expo](https://clerk.com/docs/references/expo/passkeys)
## Recommendations
This guide covers a lot of ground, from basic email and password flows to fully custom OAuth implementations, session management, and modern methods like biometrics and passkeys. Not all of these need to be implemented at once.
In many cases, starting simple is the best approach. Shipping your app with something like email authentication using a magic link or one-time passcode is often more than enough to get through the App Store review process and start collecting feedback from real users.
That said, if you're building an app where you expect high traffic from day one or need to support sign-in across platforms with minimal friction, investing in a more complete authentication flow early on can make a big difference. It can help improve user onboarding, trust, and retention right from the start.
Modern solutions like OAuth, biometrics, and passkeys are not required, but they can be excellent additions once your core system is in place.
The key is to build authentication that fits your current needs, while staying flexible enough to grow with your product.
---
---
modificationDate: February 25, 2026
title: Unit testing with Jest
description: Learn how to set up and configure the jest-expo library to write unit and snapshot tests for a project with Jest.
---
# Unit testing with Jest
Learn how to set up and configure the jest-expo library to write unit and snapshot tests for a project with Jest.
[Jest](https://jestjs.io) is the most widely used unit and snapshot JavaScript testing framework. In this guide, you will learn how to set up Jest in your project, write a unit test, write a snapshot test, and best practices for structuring your tests when using Jest with React Native.
You will also use the [`jest-expo`](https://github.com/expo/expo/tree/main/packages/jest-expo) library, which is a Jest preset that mocks the native part of the Expo SDK and handles most of the configuration required for your Expo project.
## Installation and configuration
After creating your Expo project, follow the instructions below to install and configure `jest-expo` in your project:
Install `jest-expo` and other required dev dependencies in your project. Run the following command from your project's root directory:
```sh
npx expo install jest-expo jest @types/jest --dev
```
> **Note:** If your project is not using TypeScript, you can skip installing `@types/jest`.
Open **package.json**, add a script for running tests, and add the preset for using the base configuration from `jest-expo`:
```json
{
"scripts": {
"test": "jest --watchAll"
...
```
In **package.json**, add `jest-expo` as a preset so that a base for Jest's configuration is set up:
```json
{
"jest": {
"preset": "jest-expo"
}
}
```
Additional configuration for using `transformIgnorePatterns`
You can transpile node modules your project uses by configuring [`transformIgnorePatterns`](https://jestjs.io/docs/configuration#transformignorepatterns-arraystring) in your **package.json**. This property takes a regex pattern as its value:
```json
"jest": {
"preset": "jest-expo",
"transformIgnorePatterns": [
"node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg)"
]
}
```
Jest has many configuration options, but the above configuration should cover most of your needs. However, you can always add to this pattern list. For more details, see [Configuring Jest](https://jestjs.io/docs/configuration).
## Install React Native Testing Library
The [React Native Testing Library (`@testing-library/react-native`)](https://callstack.github.io/react-native-testing-library/) is a lightweight solution for testing React Native components. It provides utility functions and works with Jest.
To install it, run the following command:
```sh
npx expo install @testing-library/react-native --dev
```
> **Deprecated:** `@testing-library/react-native` replaces the deprecated `react-test-renderer` because `react-test-renderer` does not support React 19 and above. Remove the deprecated library from your project if you are currently using it. See [React's documentation for more information](https://react.dev/warnings/react-test-renderer).
## Unit test
A unit test checks the smallest unit of code, usually a function. To write your first unit test, take a look at the following example:
Inside the **app** directory of your project, create a new file called **index.tsx**, and the following code to render a simple component:
```tsx
import { PropsWithChildren } from 'react';
import { StyleSheet, Text, View } from 'react-native';
export const CustomText = ({ children }: PropsWithChildren) => {children};
export default function HomeScreen() {
return (
Welcome!
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
```
Create a **__tests__** directory at the root of your project's directory. If this directory already exists in your project, use that. Then, create a new file called **HomeScreen-test.tsx**. The `jest-expo` preset customizes the Jest configuration to also identify files with **-test.ts|tsx** extensions as tests.
Add the following example code in **HomeScreen-test.tsx**:
```tsx
import { render } from '@testing-library/react-native';
import HomeScreen, { CustomText } from '@/app/index';
describe('', () => {
test('Text renders correctly on HomeScreen', () => {
const { getByText } = render();
getByText('Welcome!');
});
});
```
In the above example, the `getByText` query helps your tests find relevant element in your app's user interface and make assertion whether or not the certain element exists. The React Native Testing Library provides this query, and each [query variant](https://callstack.github.io/react-native-testing-library/docs/api/queries#query-variant) differs in its return type. For more examples and detailed API information, see the React Native Testing Library's [Queries API reference](https://callstack.github.io/react-native-testing-library/docs/api/queries).
Run the following command in a terminal window to execute the test:
```sh
npm run test
```
You will see one test being passed.
## Structure your tests
Organizing your test files is important to make them easier to maintain. A common pattern is creating a **__tests__** directory and putting all your tests inside.
An example structure of tests next to the **components** directory is shown below:
`__tests__`
`ThemedText-test.tsx`
`components`
`ThemedText.tsx`
`ThemedView.tsx`
Alternatively, you can have multiple **__tests__** sub-directories for different areas of your project. For example, create a separate test directory for **components**, and so on:
`components`
`ThemedText.tsx`
`__tests__`
`ThemedText-test.tsx`
`utils`
`index.tsx`
`__tests__`
`index-test.tsx`
It's all about preferences, and it is up to you to decide how you want to organize your project directory.
## Snapshot test
> **Note:** For UI testing, we recommend end-to-end tests instead of snapshot unit tests. See the [E2E tests with Maestro](/eas/workflows/examples/e2e-tests) guide.
A [snapshot test](https://jestjs.io/docs/en/snapshot-testing) is used to make sure that UI stays consistent, especially when a project is working with global styles that are potentially shared across components.
To add a snapshot test for ``, add the following code snippet in the `describe()` in **HomeScreen-test.tsx**:
```tsx
describe('', () => {
...
test('CustomText renders correctly', () => {
const tree = render(Some text).toJSON();
expect(tree).toMatchSnapshot();
});
});
```
Run `npm run test` command, and you will see a snapshot created inside **__tests__\\__snapshots__** directory, and two tests passed.
## Code coverage reports
Code coverage reports can help you understand how much of your code is tested. To see the code coverage report in your project using the HTML format, in **package.json**, under `jest`, set the `collectCoverage` to true and use `collectCoverageFrom` to specify a list of files to ignore when collecting the coverage.
```json
"jest": {
...
"collectCoverage": true,
"collectCoverageFrom": [
"**/*.{ts,tsx,js,jsx}",
"!**/coverage/**",
"!**/node_modules/**",
"!**/babel.config.js",
"!**/expo-env.d.ts",
"!**/.expo/**"
]
}
```
Run `npm run test`. You will see a **coverage** directory created in your project. Find the **lcov-report/index.html** and open it in a browser to see the coverage report.
> Usually, we don't recommend uploading **index.html** file to git. Add `coverage/**/*` in the **.gitignore** file to prevent it from being tracked.
## Jest flows (optional)
You can also use different flows to run your tests. Below are a few example scripts that you can try:
```json
"scripts": {
"test": "jest --watch --coverage=false --changedSince=origin/main",
"testDebug": "jest -o --watch --coverage=false",
"testFinal": "jest",
"updateSnapshots": "jest -u --coverage=false"
...
}
```
For more information, see [CLI Options](https://jestjs.io/docs/en/cli) in Jest documentation.
## Additional information
[React Native Testing library documentation](https://callstack.github.io/react-native-testing-library/docs/start/quick-start) — See React Native Testing Library documentation, which provides testing utilities and encourages good testing practices and work with Jest.
[Testing configuration for Expo Router](/router/reference/testing) — Learn how to create integration tests for your app when using Expo Router.
[E2E tests with EAS Workflows](/eas/workflows/examples/e2e-tests) — Learn how to set up and run E2E tests on EAS Workflows with Maestro.
---
---
modificationDate: February 25, 2026
title: Overview of distributing apps for review
description: Learn about how to distribute your app for review using app stores, internal distribution, and EAS Update.
---
# Overview of distributing apps for review
Learn about how to distribute your app for review using app stores, internal distribution, and EAS Update.
This page outlines three approaches to sharing a preview version of your app with your team for QA and review: app store testing tracks, internal distribution, and development builds with EAS Update.
Can I use Expo Go for reviewing releases?
Expo Go is a playground for students and learners, not for building production-grade projects. It's not useful for the review process of your app.
## App store testing tracks
When distributing apps through app store testing tracks, you can only use release builds. You cannot use this method to distribute development builds. An alternative approach is to use ["Internal distribution"](/review/overview#internal-distribution-with-eas-build), which works with both release and development builds.
Android: Google Play Beta
Before a complete public release, [Google Play beta](https://support.google.com/googleplay/android-developer/answer/9845334?visit_id=638740965629093187-3840249980&rd=1) is another option to distribute your app to testers. You can set up either an internal, closed, or open test track and control who has access to the app.
Each test track has its own requirements. For the internal track, you can only invite up to 100 testers. Both closed and open tracks support larger groups of testers. In closed tracks, you need to invite testers, while in open tracks, anyone can join your program.
To use Google Play beta, you need to upload your app as an AAB (Android App Bundle) to the Google Play Console, set up a test track, and invite users via email or a shareable link. Testers can install the app through the Play Store, and you can collect feedback and crash reports directly from the Google Play Console.
iOS: TestFlight
TestFlight is another option to distribute your app to iOS devices. TestFlight also requires a paid Apple Developer account. TestFlight's internal testing option allows you to create test groups that include up to 100 members of your Apple Developer account team, who then download the app through the TestFlight app. Some teams prefer TestFlight because it doesn't require a new build to add new testers, and apps stay updated automatically.
TestFlight also includes an external testing option that allows you to share your app with up to 10,000 users via an email or a public link.
Both internal and external test distribution in TestFlight require you to [upload your app](/submit/ios) to App Store Connect and wait for the automated review before you can share a build. However, external test builds will need to go through a more formal App Store review (which is distinct from the review that your app must undergo before production release) before being distributed.
[EAS Submit](/submit/introduction) — Learn how to upload your app to app store testing and release tracks.
## Internal distribution with EAS Build
[Internal distribution](/build/internal-distribution) is a feature provided by EAS that allows developers to create builds and easily share them with a URL. The URL can be opened on a device to install the app. The app is provided as an installable APK for Android or an ad hoc provisioned app for iOS.
As soon as an internal distribution build is created, it is available for download and installation — no need to fill out any forms or wait for approval/processing. You can use internal distribution to share both release and development builds.
[How to set up an internal distribution build](/build/internal-distribution) — Learn how EAS Build provides shareable URLs for your builds with your team for internal distribution.
## Development builds and EAS Update
You can use [development builds](/develop/development-builds/introduction) to load previews of your app during the review stage by publishing an update with [EAS Update](/eas-update/introduction). After sharing a development build through internal distribution and installing it, you can launch any update that you published with EAS Update, as long as it is compatible with the installed build. Learn more about [Runtime versions and updates](/eas-update/runtime-versions).
You can use the EAS dashboard to launch updates and share a link to a specific update.
You can explore and launch updates directly from a development build.
You can configure GitHub Actions to automatically publish updates on PRs and commits.
This approach is uniquely powerful because it allows you to respond to feedback as quickly as you can run `eas update`. It can take seconds to share a new version of your app with your team, and you can do so without needing to rebuild the app or upload it to a store test track.
[Get started with EAS Update](/eas-update/getting-started) — Learn how to get started using expo-updates library and use EAS Update in your project. — expo-updates
[Use GitHub Actions](/eas-update/github-actions) — Learn how to use GitHub Actions to automate the process of publishing updates with EAS Update. It also makes deploying updates consistent and fast, leaving you more time to develop your app.
[Use expo-dev-client with EAS Update](/eas-update/expo-dev-client) — expo-dev-client — Learn how to use expo-dev-client in your project to launch different app versions and preview a published update inside a development build. — expo-dev-client
---
---
modificationDate: February 25, 2026
title: Share previews with your team
description: Share previews of your app with your team by publishing updates on branches.
---
# Share previews with your team
Share previews of your app with your team by publishing updates on branches.
Once you've made changes on a branch, you can share them with your team by publishing an update. This allows you to get feedback on your changes during review.
The following steps will outline a basic flow for publishing a preview of your changes, and then sharing it with your team. For a more comprehensive resource, see the [Preview updates](/eas-update/preview) guide.
## Publish a preview of your changes
You can publish a preview of your current changes by running the following [EAS CLI](/develop/tools#eas-cli) command:
```sh
eas update --auto
```
This command will publish an update under the current branch name.
## Share with your team
Once the preview is published, you'll see output like this in the terminal window:
```sh
✔ Published!
...
EAS Dashboard https://expo.dev/accounts/your-account/projects/your-project/updates/708b05d8-9bcf-4212-a052-ce40583b04fd
```
Share the **EAS dashboard** link with a reviewer. After opening the link, they can click on the **Preview** button. They will see a QR code that they can scan to open the preview on their device.
## Create previews automatically
You can automatically create previews on every commit with [EAS Workflows](/eas/workflows/introduction). First, you'll need to [configure your project](/eas/workflows/get-started), add a file named **.eas/workflows/publish-preview-update.yml** at the root of your project, then add the following workflow configuration:
```yaml
name: Publish preview update
on:
push:
branches: ['*']
jobs:
publish_preview_update:
name: Publish preview update
type: update
params:
branch: ${{ github.ref_name || 'test' }}
```
The workflow above will publish an update on every commit to every branch. You can also run this workflow manually with the following EAS CLI command:
```sh
eas workflow:run publish-preview-update.yml
```
Learn more about common patterns with the [workflows examples guide](/eas/workflows/examples).
## Learn more
[Preview updates](/eas-update/preview) — Learn how to preview updates in development, preview, and production builds.
---
---
modificationDate: February 25, 2026
title: How to launch an update using Expo Orbit
description: Learn how to open updates with Expo Orbit as part of a review workflow.
---
# How to launch an update using Expo Orbit
Learn how to open updates with Expo Orbit as part of a review workflow.
[Expo Orbit](https://expo.dev/orbit) is a macOS and Windows app designed to speed up installing and running builds from EAS. It makes running your builds and updates as easy as pressing **Open in Orbit**.
How does automatic installation and launching of updates work?
When you launch an update, Orbit will look for the latest development build that matches the runtime version and target platform of the update. If a compatible build is found, the update will install automatically on the target device and launch with a deep link that points to the update.
If you don't have any development builds available, either because they have all expired, you haven't created one, you don't use EAS Build, or you are [building your app locally](/guides/local-app-development), then Orbit will prompt you on how to proceed. Click **Launch with deep link** in the prompt to open the update if you already have a compatible development build installed on your target device.
## Prerequisites
- **Install the Orbit app** before following the steps in this guide. You can download it directly from [GitHub releases](https://github.com/expo/orbit/releases) or see the [alternative method](/build/orbit#installation) to install it.
- After installing the app, sign in to your Expo account from **Settings**.
## Preview an update with Expo Orbit
Expo Orbit launching an update directly from EAS dashboard to an iOS Simulator.
Previewing with Expo Orbit requires you to have an update published. If you haven't published an update, see [Publish an update](/eas-update/getting-started#publish-an-update) before following the steps in the next section.
### Install and launch the update
> **Note**: Launching updates using Expo Orbit is not supported on physical iOS devices. It is supported on Android devices/emulators or iOS Simulators.
After the update is published, follow these steps to open it on an Android Emulator or iOS Simulator:
- Navigate your project's **Updates** tab.
- Select the update you want to preview.
- Click **Preview**. This will open the **Preview** dialog.
- Under **Open with Orbit**, select a platform to launch the update.
- Orbit will install and launch the update on the selected Android Emulator or iOS Simulator.
You can now seamlessly launch and review updates using Expo Orbit.
---
---
modificationDate: February 25, 2026
title: Build your project for app stores
description: Learn how to create a production build for your app that is ready to be submitted to app stores from the command line using EAS Build.
---
# Build your project for app stores
Learn how to create a production build for your app that is ready to be submitted to app stores from the command line using EAS Build.
Whether you have built a native app binary using [EAS](/build/setup) or [locally](/guides/local-app-development), the next step in your app development journey is to submit your app to the stores. To do so, you need to create a **production build**.
Production builds are submitted to app stores for release to the general public or as part of a store-facilitated testing process such as TestFlight. This guide explains how to create production builds with [EAS](/deploy/build-project#production-builds-using-eas) and [locally](/deploy/build-project#production-builds-locally). It is also possible to create production builds for Expo apps with any CI service capable of compiling Android and iOS apps.
## Production builds using EAS
Production builds must be installed through their respective app stores. You cannot install them directly on your Android Emulator, iOS Emulator, or device. The only exception to this is if you explicitly set `"buildType": "apk"` for Android on your build profile. However, it is recommended to use **aab** when submitting to stores, and this is the default configuration.
### `eas.json` configuration
A minimal configuration for building a production build in **eas.json** is already created when you create your first build:
```json
{
"build": {
...
"production": {}
...
}
}
```
### Create a production build
To create a production build, run the following command for a platform:
```sh
eas build --platform android
```
You can attach a message to the build by passing `--message` to the build command, for example, `eas build --platform ios --message "Some message"`. The message will appear on the EAS dashboard. It comes in handy when you want to specify the purpose of the build for your team.
Alternatively, you can use `--platform all` option to build for Android and iOS at the same time:
```sh
eas build --platform all
```
## Developer account
You will need to have a developer account for the app store you want to submit your app.
Google Play Developer membership is required to distribute to the Google Play Store.
You can build and sign your app using EAS Build, but you can't upload it to the Google Play Store unless you have a membership, a one-time $25 USD fee.
Apple Developer Program membership is required to build for the Apple App Store.
If you are going to use EAS Build to create production builds for the Apple App Store, you need access to an account with a $99 USD [Apple Developer Program](https://developer.apple.com/programs) membership.
## App signing credentials
Before the build process can start for app stores, you need a store developer account and generate or provide app signing credentials.
Whether you have experience with generating app signing credentials or not, EAS CLI can do the heavy lifting. You can opt-in for EAS CLI to handle the app signing credentials process.
### Android app signing credentials
- If you have not yet generated a keystore for your app, use EAS CLI by selecting `Generate new keystore`, and then you are done. The keystore is stored securely on EAS servers.
- If you want to manually generate your keystore, see the [manual Android credentials guide](/app-signing/local-credentials#android-credentials) for more information.
### iOS app signing credentials
- If you have not generated a provisioning profile and/or distribution certificate yet, use EAS CLI by signing in to your Apple Developer Program account and following the prompts.
- If you want to manually generate your credentials, see the [manual iOS credentials guide](/app-signing/local-credentials#ios-credentials) for more information.
## Wait for the build to complete
By default, the `eas build` command will wait for your build to complete, but you can interrupt it if you prefer not to wait. Instead, use the builds details page link prompted by EAS CLI to monitor the build progress and read the build logs. You can also find this page by visiting [your build dashboard](https://expo.dev/builds) or running the following command:
```sh
eas build:list
```
If you are a member of an organization and your build is on its behalf, you will find the build details on [the build dashboard for that account](https://expo.dev/accounts/%5Baccount%5D/builds).
## Create builds automatically
You can automatically create builds on commits to specific branches with [EAS Workflows](/eas/workflows/introduction). First, you'll need to [configure your project](/eas/workflows/get-started), add a file named **.eas/workflows/create-builds.yml** at the root of your project, then add the following workflow configuration:
```yaml
name: Create builds
on:
push:
branches: ['main']
jobs:
build_android:
name: Build Android app
type: build
params:
platform: android
profile: production
build_ios:
name: Build iOS app
type: build
params:
platform: ios
profile: production
```
The workflow above will create Android and iOS builds on every commit to your project's `main` branch. You can also run this workflow manually with the following EAS CLI command:
```sh
eas workflow:run create-builds.yml
```
Learn more about common patterns with the [workflows examples guide](/eas/workflows/examples).
## Release builds locally
To create a release (also known as production) build locally, see the following React Native guides for more information on the necessary steps for Android and iOS.
These guides assume your project has **android** and/or **ios** directories containing the respective native projects. If you use [Continuous Native Generation](/workflow/continuous-native-generation) then you will need to run [prebuild](/workflow/prebuild) to generate the directories before following the guides.
> **Note**: Following the guide below, in step four, when you build the release **.aab** for Android, run `./gradlew app:bundleRelease` from the **android** directory instead of `npx react-native build-android --mode=release`.
[Publishing to Google Play Store](https://reactnative.dev/docs/signed-apk-android) — Learn how to publish an app to Google Play Store by following the necessary steps manually.
[Publishing to Apple App Store](https://reactnative.dev/docs/publishing-to-app-store) — Learn how to publish an app to Apple App Store by following the necessary steps manually.
## Next step
[App stores best practices](/distribution/app-stores) — Learn about the best practices for submitting your app to app stores.
---
---
modificationDate: February 25, 2026
title: Submit to app stores
description: Learn how to submit your app to Google Play Store and Apple App Store from the command line with EAS Submit.
---
# Submit to app stores
Learn how to submit your app to Google Play Store and Apple App Store from the command line with EAS Submit.
**EAS Submit** is a hosted service that allows uploading and submitting app binaries to the app stores using EAS CLI. This guide describes how to submit your app to the Google Play Store and Apple App Store using EAS Submit.
[How to quickly publish to the App Store & Play Store with EAS Submit](https://www.youtube.com/watch?v=-KZjr576tuE) — EAS Submit makes it easy to publish your apps to the App Store and Play Store with a simple command.
## Apple App Store
Prerequisites
4 requirements
1.
Sign up for an Apple Developer account
An Apple Developer account is required to submit your app to the Apple App Store. You can sign up for an Apple Developer account on the [Apple Developer Portal](https://developer.apple.com/account/).
2.
Include a bundle identifier in app.json
Include your app's bundle identifier in **app.json**:
```json
{
"ios": {
"bundleIdentifier": "com.yourcompany.yourapp"
}
}
```
3.
Install EAS CLI and authenticate with your Expo account
Install EAS CLI and login with your Expo account:
```sh
npm install -g eas-cli && eas login
```
4.
Build a production app
You'll need a production build ready for store submission. You can create one using [EAS Build](/build/introduction):
```sh
eas build --platform ios --profile production
```
Alternatively, you can build the app on your own computer with `eas build --platform ios --profile production --local` or with Xcode.
Once you have completed all the prerequisites, you can start the submission process.
Run the following command to submit a build to the Apple App Store:
```sh
eas submit --platform ios
```
The command will lead you step by step through the process of submitting the app.
## Google Play Store
Prerequisites
7 requirements
1.
Sign up for a Google Play Developer account
A Google Play Developer account is required to submit your app to the Google Play Store. You can sign up for a Google Play Developer account on the [Google Play Console sign-up page](https://play.google.com/apps/publish/signup/).
2.
Create a Google Service Account
EAS requires you to upload and configure a Google Service Account Key to submit your Android app to the Google Play Store. You can create one with the [uploading a Google Service Account Key for Play Store submissions with EAS](https://github.com/expo/fyi/blob/main/creating-google-service-account.md) guide.
3.
Create an app on Google Play Console
Create an app by clicking **Create app** in the [Google Play Console](https://play.google.com/apps/publish/).
4.
Install EAS CLI and authenticate with your Expo account
Install EAS CLI and login with your Expo account:
```sh
npm install -g eas-cli && eas login
```
5.
Include a package name in app.json
Include your app's package name in **app.json**:
```json
{
"android": {
"package": "com.yourcompany.yourapp"
}
}
```
6.
Build a production app
You'll need a production build ready for store submission. You can create one using [EAS Build](/build/introduction):
```sh
eas build --platform android --profile production
```
Alternatively, you can build the app on your own computer with `eas build --platform android --profile production --local` or with Android Studio.
7.
Upload your app manually at least once
You have to upload your app manually at least once. This is a limitation of the Google Play Store API.
Learn how with the [first submission of an Android app](https://expo.fyi/first-android-submission) guide.
Once you have completed all the prerequisites, you can start the submission process.
Run the following command to submit a build to the Google Play Store:
```sh
eas submit --platform android
```
The command will lead you step by step through the process of submitting the app.
## Build and submit automatically
You can automatically create builds and submit them to the app stores with [EAS Workflows](/eas/workflows/introduction). First, you'll need to [configure your project](/eas/workflows/get-started), add a file named **.eas/workflows/build-and-submit.yml** at the root of your project, then add the following workflow configuration:
```yaml
name: Build and submit
on:
push:
branches: ['main']
jobs:
build_android:
name: Build Android app
type: build
params:
platform: android
profile: production
build_ios:
name: Build iOS app
type: build
params:
platform: ios
profile: production
submit_android:
name: Submit Android
type: submit
needs: [build_android]
params:
build_id: ${{ needs.build_android.outputs.build_id }}
submit_ios:
name: Submit iOS
type: submit
needs: [build_ios]
params:
build_id: ${{ needs.build_ios.outputs.build_id }}
```
The workflow above will create Android and iOS builds on every commit to your project's `main` branch, then submit them to the Google Play and Apple App Store respectively. You can also run this workflow manually with the following EAS CLI command:
```sh
eas workflow:run build-and-submit.yml
```
Learn more about common patterns with the [workflows examples guide](/eas/workflows/examples).
## Manual submission to app stores
You can also submit your app manually to the Google Play Store and Apple App Store.
[Manual Apple App Store submission](/guides/local-app-production#app-submission-using-app-store-connect) — Learn how to submit your app manually to Apple App Store.
[Manual Google Play Store submission](https://expo.fyi/first-android-submission) — Follow the steps on manually submitting your app to Google Play Store.
## Next step
[Configure EAS Submit with eas.json](/submit/eas-json) — Learn how to pre-configure your project using eas.json file with EAS Submit and more about Android or iOS specific options.
---
---
modificationDate: February 25, 2026
title: App stores metadata
description: A brief overview of how to use EAS Metadata to automate and maintain your app store presence.
---
# App stores metadata
A brief overview of how to use EAS Metadata to automate and maintain your app store presence.
> **EAS Metadata** is in beta and subject to breaking changes.
When submitting your app to app stores, you need to provide metadata. This process is lengthy and is often about complex topics that don't apply to your app. After the information you provide gets reviewed and if there is any issue with it, you need to restart this process.
[**EAS Metadata**](/eas/metadata) enables you to automate and maintain this information from the command line instead of going through multiple forms in the app store dashboards. It can also instantly identify well-known app store restrictions that could trigger a rejection after a lengthy review queue. This guide shows how to use EAS Metadata to automate and maintain your app store presence.
## Prerequisites
EAS Metadata currently **only supports the Apple App Store**.
> Using VS Code? Install the [Expo Tools extension](https://github.com/expo/vscode-expo#readme) for auto-complete, suggestions, and warnings in your **store.config.json** files.
## Create a store config
EAS Metadata uses [store.config.json](/eas/metadata/config) file to hold all the information you want to upload to the app stores. This file is located at the root of your Expo project.
Create a new **store.config.json** file at the root of your project directory as shown in the example below:
```json
{
"configVersion": 0,
"apple": {
"info": {
"en-US": {
"title": "Awesome App",
"subtitle": "Your self-made awesome app",
"description": "The most awesome app you have ever seen",
"keywords": ["awesome", "app"],
"marketingUrl": "https://example.com/en/promo",
"supportUrl": "https://example.com/en/support",
"privacyPolicyUrl": "https://example.com/en/privacy"
}
}
}
}
```
The above example file contains JSON schema. Replace the example values with your own. It is usually contains your app's `title`, `subtitle` , `description`, `keywords`, and `marketingUrl` and so on.
**An important thing to remember from the above example is the `configVersion` property.** It helps with versioning changes that are not backward compatible.
> For more information on properties that can be defined in **store.config.json**, see [Schema for EAS Metadata](/eas/metadata/schema#config-schema).
## Upload the store config
> Before pushing the **store.config.json** to the app stores, you must upload a new binary of your app. See [App Store submissions](/deploy/submit-to-app-stores) for more information. After the binary is submitted and processed, you can continue with the step below.
After you have created the **store.config.json** file and added the necessary information related to your app, you can push the store config to the app stores by running the command:
```sh
eas metadata:push
```
If EAS Metadata runs into any issues with your store config, it will warn you when running this command. When there are no errors, or you confirm to push it with possible issues, it will try to upload as much as possible.
You can also re-use this command when you modify the **store.config.json** file and want to push the latest changes to the app stores.
## Next steps
[EAS Metadata schema](/eas/metadata/schema) — A reference of store config in EAS Metadata.
[Static and dynamic configurations with EAS Metadata](/eas/metadata/config) — Learn about different ways to configure EAS Metadata.
---
---
modificationDate: February 25, 2026
title: Send over-the-air updates
description: Learn how to send over-the-air updates to push critical bug fixes and improvements to your users.
---
# Send over-the-air updates
Learn how to send over-the-air updates to push critical bug fixes and improvements to your users.
You can send over-the-air updates containing critical bug fixes and improvements to your users.
## Get started
> If you've published [previews](/review/share-previews-with-your-team) or created a [build](/deploy/build-project) before, you may have already set up updates and can skip this section.
To set up updates, run the following [EAS CLI](/develop/tools#eas-cli) command:
```sh
eas update:configure
```
After the command completes, you'll need to make new builds before continuing to the next section.
## Send an update
To send an update, run the following [EAS CLI](/develop/tools#eas-cli) command:
```sh
eas update --channel production
```
This command will create an update and make it available to builds of your app that are configured to receive updates on the `production` channel. This channel is defined in [**eas.json**](/eas/json#channel).
You can verify the update works by force closing the app and reopening it two times. The update should be applied on the second launch.
## Send updates automatically
You can automatically send updates with [EAS Workflows](/eas/workflows/introduction). First, you'll need to [configure your project](/eas/workflows/get-started), add a file named **.eas/workflows/send-updates.yml** at the root of your project, then add the following workflow configuration:
```yaml
name: Send updates
on:
push:
branches: ['main']
jobs:
send_updates:
name: Send updates
type: update
params:
channel: production
```
The workflow above will send an over-the-air update for the `production` update channel on every commit to your project's `main` branch. You can also run this workflow manually with the following EAS CLI command:
```sh
eas workflow:run send-updates.yml
```
Learn more about common patterns with the [workflows examples guide](/eas/workflows/examples).
## Learn more
You can learn how to [rollout an update](/eas-update/rollouts), [optimize assets](/eas-update/optimize-assets), and more with our [update guides](/eas-update/introduction).
---
---
modificationDate: February 25, 2026
title: Publish your web app
description: Learn how to deploy your web app using EAS Hosting.
---
# Publish your web app
Learn how to deploy your web app using EAS Hosting.
If you are building a universal app, you can quickly deploy your web app using [EAS Hosting](/eas/hosting/introduction). It is a service for deploying web apps built with Expo Router and React.
## Prerequisites
Before you begin, in your project's **app.json** file, ensure that the [`expo.web.output`](/versions/latest/config/app#output) property is either `static` or `server`.
## Export your web project
To deploy your web app, you need to create a static build of your web project. Export your web project into a **dist** directory by running the following command:
```sh
npx expo export --platform web
```
> Remember to re-run this command every time before deploying when you make changes to your web app.
## Initial deployment
To publish your web app, run the following [EAS CLI](/develop/tools#eas-cli) command:
```sh
eas deploy
```
After running this command for the first time, you'll be prompted to select a preview subdomain for your project. This subdomain is a prefix used to create a preview URL and is used for production deployments. For example, in `https://test-app--1234.expo.app`, `test-app` is the preview subdomain.
Once your deployment is complete, the EAS CLI will output a preview URL to access your deployed app.
## Production deployment
To create a production deployment, run the following [EAS CLI](/develop/tools#eas-cli) command:
```sh
eas deploy --prod
```
Once your deployment is complete, the EAS CLI will output a production URL to access your deployed app.
## Deploy automatically
You can automatically deploy your app to the web with [EAS Workflows](/eas/workflows/introduction). First, you'll need to [configure your project](/eas/workflows/get-started), add a file named **.eas/workflows/deploy-web.yml** at the root of your project, then add the following workflow configuration:
```yaml
name: Deploy web
on:
push:
branches: ['main']
jobs:
deploy_web:
name: Deploy web
type: deploy
params:
prod: true
```
The workflow above will create a web deployment on every commit to your project's `main` branch. You can also run this workflow manually with the following EAS CLI command:
```sh
eas workflow:run deploy-web.yml
```
Learn more about common patterns with the [workflows examples guide](/eas/workflows/examples).
## Learn more
You can learn more about setting up [deployment aliases](/eas/hosting/deployments-and-aliases), using a [custom domain](/eas/hosting/custom-domain), or [deploying an API Route](/router/web/api-routes#deployment).
---
---
modificationDate: February 25, 2026
title: Monitoring services
description: Learn how to monitor the usage of your Expo and React Native app after its release.
---
# Monitoring services
Learn how to monitor the usage of your Expo and React Native app after its release.
Once your app is released, you can track anonymized usage data to give you insights on how users use your app. This data includes which updates are in use, when users experience bugs, and more.
## EAS Insights
Expo provides the [`expo-insights`](/eas-insights/introduction) library, which tracks information related to [EAS Update](/deploy/send-over-the-air-updates). This data includes the app version, platform, OS version, and update adoption. After you install it and release production builds on the app stores, you'll be able to see additional data on your project dashboard:
Get started with the following guide:
[EAS Insights](/eas-insights/introduction) — Learn how to use EAS Insights to monitor your app.
## LogRocket
You can get more insights with [LogRocket](https://logrocket.com). LogRocket records user sessions and identifies bugs as your users use your app. You can filter sessions by update IDs and also connect to your LogRocket account on the EAS dashboard to get quick access to your app's session data.
Get started with the following guide:
[Using LogRocket](/guides/using-logrocket) — Learn how to use LogRocket to monitor your app.
## Sentry
[Sentry](http://getsentry.com/) is a crash reporting platform that provides real-time insight into production deployments with information to reproduce and fix crashes.
It notifies you of exceptions or errors that your users run into while using your app and organizes them for you on a web dashboard. Reported exceptions include stacktraces, device info, version, and other relevant context automatically. You can also provide additional context that is specific to your app, such as the current route and user ID.
Get started with the following guide:
[Using Sentry](/guides/using-sentry) — Learn how to use Sentry to monitor your app.
## Vexo
[Vexo](https://www.vexo.co/) helps you understand how users interact with your Expo app, identify friction points, and improve engagement. It provides real-time user analytics with a simple two-line integration and offers a complete dashboard with insights into user activity, app performance, and adoption trends, along with features like heatmaps, session replays, and more.
Get started with the following guide:
[Using Vexo](/guides/using-vexo) — Learn how to use Vexo to monitor your app.
## BugSnag
[BugSnag](https://www.bugsnag.com/) is a stability monitoring solution that provides rich, end-to-end error reporting and analytics to reproduce and fix errors with speed and precision. BugSnag supports the full stack with open-source libraries for more than 50 platforms, including React Native.
Get started with the following guide:
[Using BugSnag](/guides/using-bugsnag) — Learn how to use BugSnag to monitor your app.
---
---
modificationDate: February 25, 2026
title: Core concepts
description: An overview of Expo tools, features and services.
---
# Core concepts
An overview of Expo tools, features and services.
Expo is an [open-source framework](https://github.com/expo/expo/) for apps that run natively on Android, iOS, and the web. Expo brings together the best of mobile and the web and enables many important features for building and scaling an app.
The `expo` npm package enables a suite of incredible features for React Native apps. The `expo` package can be installed in nearly **any React Native project**.
## Tools and features
[Expo SDK](/versions/latest) — Comprehensive suite of well-tested React Native modules that run on Android, iOS, and web.
[Develop an app with Expo](/workflow/overview) — An overview of the development process of building an Expo app to help build a mental model of the core development loop.
[Expo Modules API](/modules/overview) — Write highly performant native code with modern Swift and Kotlin API.
[Prebuild](/workflow/prebuild) — Separate React from Native to develop from any computer, upgrade easily, white label apps, and maintain larger projects.
[Expo CLI](/more/expo-cli) — Manage dependencies, compile native apps, develop for the web, and connect to any device with a powerful dev server.
[Expo Go](/get-started/set-up-your-environment) — A playground for students and learners to try React Native on a simulator or device.
> All features are free, optional, and can be used independently of each other. Unused features add no additional bloat to your app.
| Feature | With `expo` | Without `expo` (bare React Native) |
| --- | --- | --- |
| Develop complex apps **entirely** in JavaScript. | ✓ | ✗ |
| Write JSI native modules with Swift and Kotlin. | ✓ | ✗ |
| Develop apps without Xcode or Android Studio. | ✓ | ✗ |
| Create and share example apps in the browser with [Snack](https://snack.expo.dev/). | ✓ | ✗ |
| Major upgrades without native changes. | ✓ | ✗ |
| First-class TypeScript support. | ✓ | ✗ |
| Install natively compatible libraries from the command line. | ✓ | ✗ |
| Develop performant websites with the same codebase. | ✓ | ✗ |
| [Tunnel](/more/expo-cli#tunneling) your dev server to any device. | ✓ | ✗ |
## Services
The team behind Expo also provides **Expo Application Services (EAS)**, deeply integrated cloud services for building, submitting, and updating your React Native app. EAS can be used with **any React Native app**, regardless of whether it uses `expo` or not.
[Expo Application Services](/eas) — The easiest way to build, deploy, and update native apps.
---
---
modificationDate: February 25, 2026
title: FAQ
description: A list of common questions and limitations about Expo and related services.
---
# FAQ
A list of common questions and limitations about Expo and related services.
This page lists some of the common questions and answers about Expo and related services. If you have a question that is not answered here, see [Forums](https://chat.expo.dev/) for more common questions.
## What is Expo used for?
Expo is an [open-source framework](https://github.com/expo/expo) for apps that run natively on Android, iOS, and the web. Expo brings together the best of mobile and the web and enables many important features for building and scaling an app such as live updates, instantly sharing your app, and web support. The `expo` npm package enables a suite of incredible features for React Native apps. The `expo` package can be installed in nearly any React Native project. See [what Expo offers](/core-concepts) for more information.
## Do companies use Expo?
Yes, Expo is used by top companies worldwide, serving hundreds of millions of end users. See our [showcase](https://expo.dev/customers).
## Why does Expo have its own SDK?
When Expo was first created, React Native had yet to be publicly released. This means there were no third-party packages. To make React Native's developer experience reasonable, we created [several libraries to achieve common functionalities](/versions/latest). Many of these libraries have since been forked and modified to meet various needs. We welcome users to mix and match whichever [custom native code](/workflow/customizing) they need to make their app great.
The Expo SDK is well-tested, written in TypeScript, documented, and built for Android, iOS, and the web. Every module in the Expo SDK works together to ensure versioning always matches. This creates a nice upgrading experience.
The Expo SDK is also written with the [Expo Modules API](/modules) to make contributing, maintaining, and understanding easier.
## What is the difference between Expo and React Native?
The `expo` package provides a suite of features that make it easier to develop, and scale complex React Native applications. You can install `expo` in nearly any React Native app. The `expo` package is not required to use [Expo Application Services (EAS)](/eas) or React Native, however, it is highly recommended. See [what Expo offers](/core-concepts) for more information.
## Do I need to switch from React Native to use Expo?
No, the `expo` npm package and CLI work with any React Native app. [Expo Application Services (EAS)](/eas) also works with all React Native apps with first-class support for builds, updates, app store submissions, and more.
## How much does Expo cost?
The Expo platform is [free and open source](https://blog.expo.dev/exponent-is-free-as-in-and-as-in-1d6d948a60dc). This includes the libraries that make up the [Expo SDK](/versions/latest) and the [Expo CLI](/more/expo-cli) used for development. The Expo Go app, the easiest way to get started, is also free from the app stores.
[Expo Application Services (EAS)](/eas) is an optional suite of cloud services for React Native apps, from the Expo team. EAS makes it easier to build your app, submit it to the stores, keep it updated, send push notifications, and more. You can use EAS for free if the [Free plan](https://expo.dev/pricing) quotas are sufficient for your app. More information is available on the [pricing page](https://expo.dev/pricing).
## How do I add custom native code to my Expo project?
Expo supports adding custom native code and customizing that native code (Android/Xcode projects). To use any custom native code, you can create a [development build](/develop/development-builds/introduction) and [config plugins](/config-plugins/introduction). We do recommend using the modules in the [Expo SDK](/versions/latest) when possible for easier upgrades and improved developer experience.
## Can I use Expo in the app that is created with React Native CLI?
Yes! All Expo tools and services work great in any React Native app. For example, you can use any part of the [Expo SDK](/versions/latest), [`expo-dev-client`](/develop/development-builds/installation) and EAS Build, Submit, and Update — they work great! Learn more about [installing `expo` in your project](/bare/installing-expo-modules), [adopting prebuild](/guides/adopting-prebuild), and [setting up EAS Build](/build/introduction).
## How do I share my Expo project? Can I submit it to the app stores?
The fastest way to share your project is to publish with [EAS Update](/eas-update/introduction) and launch in a [development build](/develop/development-builds/introduction). This gives your app a URL; you can share this URL with anybody who has the [development build](/develop/development-builds/introduction) for Android or iOS. URLs can also be opened in Expo Go for Android.
When ready, you can create a production build (**.aab** and **.ipa**) to submit to the app stores. You can build your app in a single command with [EAS Build](/build/introduction) and submit it to the stores with [EAS Submit](/submit/introduction).
You can also use [internal distribution](/build/internal-distribution) to share your app with an APK on Android and ad-hoc or enterprise provisioning on iOS.
## Can I develop iOS apps on a Windows computer?
Traditionally you needed a macOS to develop iOS apps, however, you can use [EAS Build](/build/introduction) to build your app in the cloud. You can also use [EAS Submit](/submit/introduction) to submit your app to the stores. Testing can be done on a physical iOS device using [Expo Go](https://expo.dev/go) or a [development build](/develop/development-builds/introduction).
## What versions of Android and iOS are supported by the Expo SDK?
Currently, Expo SDK supports Android 7+ and iOS 15.1+. For more information, see [Support for Android and iOS versions](/versions/latest#support-for-android-and-ios-versions).
## What is the minimal size of a "hello world" expo app?
A bare minimum production app created using pure Expo is less than 3 MB. For iOS, Expo targets a newer minimum iOS version which enables app store optimizations.
If the `expo` package is included in your app, it only adds 1 MB one time to the final size of apps on app stores. The `expo` package has a marginal size cost (for example, 150 Kib on Android). The rest of the size comes from the language runtime (such as Kotlin on Android).
## Can I use Expo with my native library?
You can use native Android and iOS libraries with Expo by creating a [custom native module](/modules) with Swift and Kotlin. Many popular libraries already have custom native modules. Check out our [React Native directory](https://reactnative.directory) to find popular libraries for your use case.
## Can I use Expo with this web library?
Many popular web packages such as three.js work with Expo and React Native. See [Expo examples](https://github.com/expo/examples) for more information.
## Is Expo similar to React for web development?
Expo is an [open-source framework](https://github.com/expo/expo) for apps that run natively on Android, iOS, and the web. React Native is similar to `react-dom` from web development, enabling you to run React on a particular platform, however, it has a few key differences:
- React Native does not support HTML or CSS.
- Instead of using the DOM, React Native uses native components. For example, `` instead of ``. Native components are more performant than the DOM and provide a much nicer user experience.
- Unlike React.js which has access to browser APIs, React Native uses custom native APIs. For example, instead of `navigator.geolocation`, you use `expo-location` to access the user's location. Custom native APIs are similar to browser APIs except you have full control over them. This means you can access new features before they are available in the browser.
In the same way React.js frameworks help users create larger websites with ease, Expo helps users create larger apps with ease. Expo provides a suite of well-tested React Native modules that run on Android, iOS, and the web. Expo also provides a [suite of tools](/eas) for building, deploying, and updating your app.
## What are the store policies regarding interpreted code?
React Native uses a JavaScript interpreter (JSC, V8, or Hermes) to run your application code. Refer to the [Google Play Policy Center](https://play.google/developer-content-policy/) and [Apple Developer Program License Agreement](https://developer.apple.com/support/terms/apple-developer-program-license-agreement) directly for the most up-to-date policy information.
_The following are excerpts of related policies, as of April 25, 2024._
### Google Play Store
```text
...an app may not download executable code (such as dex, JAR, .so files) from a
source other than Google Play. This restriction does not apply to code that runs
in a virtual machine or an interpreter where either provides indirect access to
Android APIs (such as JavaScript in a webview or browser).
Apps or third-party code, like SDKs, with interpreted languages (JavaScript,
Python, Lua, etc.) loaded at run time (for example, not packaged with the app)
must not allow potential violations of Google Play policies.
```
Source: [Google Play Policy Center](https://support.google.com/googleplay/android-developer/answer/9888379?hl=en).
### Apple App Store
```text
...Interpreted code may be downloaded to an Application but only so long as such code:
(a) does not change the primary purpose of the Application by providing features
or functionality that are inconsistent with the intended and advertised purpose
of the Application as submitted to the App Store,
(b) does not create a store or storefront for other code or applications, and
(c) does not bypass signing, sandbox, or other security features of the OS.
```
Source: [3.3.1 APIs and Functionality - B. Executable Code](https://developer.apple.com/support/terms/apple-developer-program-license-agreement#b331).
## Should I use Expo CLI or React Native Community CLI?
Expo CLI offers the same core functionality as React Native Community CLI (also known as "React Native CLI") with additional features such as automatic [TypeScript setup](/guides/typescript), [web support](/workflow/web), [auto installing compatible libraries](/more/expo-cli#install), [improved native build commands](/more/expo-cli#compiling), [tunneling](/more/expo-cli#tunneling), [Prebuild](/workflow/prebuild), and [more](/more/expo-cli).
It can be used simultaneously with React Native Community. Regardless of which CLI you use, you can use any part of the [Expo SDK](/versions/latest) and [Expo Application Services](/eas) with your project. For more information, see:
- Learn how you can migrate to use Expo CLI in an [existing React Native project](/bare/using-expo-cli).
- Learn about the benefits of [using a framework to build React Native apps](https://reactnative.dev/blog/2024/06/25/use-a-framework-to-build-react-native-apps).
- Learn about the benefits of migrating to Expo CLI such as improved app performance, expedites release, and fostering stronger collaboration across you team in [this blog post](https://expo.dev/blog/from-rnc-cli-to-expo).
> **Note:** EAS Build is compatible with existing React Native projects (where native directories are checked into version control). When these directories are present, EAS Build does not run the prebuild step, as that could overwrite any manual customizations you have made to the native project files. You'll have to configure the native directories on your own with native tools such as Android Studio or Xcode.
## Is Expo Go open source?
Yes, the source for Expo Go can be found in the [expo/expo GitHub repository](https://github.com/expo/expo) in the **apps/expo-go** directory. The Expo Go app is also built with Expo and React Native.
## What can I do or cannot do with Expo Go?
[Expo Go](/get-started/set-up-your-environment?redirected=#how-would-you-like-to-develop) is a playground for students and learners to test out Expo quickly and understand the basics. It allows you to use libraries included in the Expo SDK and libraries that don't require custom native code.
Expo Go cannot use third-party libraries that require custom native code and you cannot edit native code directly in Expo Go. It's limited and not useful for building production-grade projects.
**We strongly recommend using [development builds](/develop/development-builds/introduction) for any real project. It's like creating a version of Expo Go that is specifically customized to your app's needs.**
## Is ejecting deprecated?
Yes, eject is a deprecated term and is no longer necessary. When Expo was first released, apps had larger native binary sizes and didn't support custom native code without "ejecting". This changed in December 2020 with the release of [EAS Build](/build/introduction) which supports any React Native app. The concept of "ejecting" was replaced by the [`npx expo prebuild`](/workflow/prebuild) command in SDK 41 (April 2021), which continuously generates native projects based on the libraries in your project and the app config (**app.json**). The `expo eject` command was fully deprecated in SDK 46 (August 2022).
Unlike the previous eject workflow, authors can configure their libraries to work with Expo Prebuild by creating a [config plugin](/config-plugins/introduction). This means you can use any library with Expo Prebuild. You can also use any custom native code with Expo Prebuild by creating a [development build](/develop/development-builds/introduction). Learn more in the [Expo Prebuild documentation](/workflow/prebuild).
---
---
modificationDate: February 25, 2026
title: Documentation for LLMs
description: A list of Expo and EAS documentation files available for large language models (LLMs) and apps that use them.
---
# Documentation for LLMs
A list of Expo and EAS documentation files available for large language models (LLMs) and apps that use them.
At Expo, we support the [llms.txt](https://llmstxt.org/) initiative to provide documentation for large language models (LLMs) and apps that use them. Below is a list of documentation files available:
- [/llms.txt](/llms.txt): A list of all available documentation files
- [/llms-full.txt](/llms-full.txt): Complete documentation for Expo, including Expo Router, Expo Modules API, development process, and more
- [/llms-eas.txt](/llms-eas.txt): Complete documentation for the Expo Application Services (EAS)
- [/llms-sdk.txt](/llms-sdk.txt): Complete documentation for the latest Expo SDK
Looking for deprecated Expo SDK versions?
- [/llms-sdk-v53.0.0.txt](/llms-sdk-v53.0.0.txt): Documentation for the Expo SDK v53.0.0
- [/llms-sdk-v52.0.0.txt](/llms-sdk-v52.0.0.txt): Documentation for the Expo SDK v52.0.0
- [/llms-sdk-v51.0.0.txt](/llms-sdk-v51.0.0.txt): Documentation for the Expo SDK v51.0.0
---
---
modificationDate: February 25, 2026
title: Overview of tutorials and UI guides
---
# Overview of tutorials and UI guides
The **Learn** section is a collection of tutorials and other guides that help you learn about Expo and EAS. It covers the following:
[Expo Tutorial](/tutorial/introduction) — If you are new to Expo, we recommend starting with this tutorial. It provides a step-by-step guide on how to build an Expo app that runs on Android, iOS and web.
[EAS Tutorial](/tutorial/eas/introduction) — If you are looking to learn about building your Android and iOS apps using Expo Application Services (EAS), this tutorial covers the EAS Build, Update, and Submit workflows.
---
---
modificationDate: February 25, 2026
title: 'Tutorial: Using React Native and Expo'
description: An introduction to a React Native tutorial on how to build a universal app that runs on Android, iOS and the web using Expo.
---
# Tutorial: Using React Native and Expo
An introduction to a React Native tutorial on how to build a universal app that runs on Android, iOS and the web using Expo.
We're about to embark on a journey of building universal apps. In this tutorial, we'll create an Expo app that runs on Android, iOS, and web; all with a single codebase. Let's get started!
## About React Native and Expo tutorial
The objective of this tutorial is to get started with Expo and become familiar with the Expo SDK. It'll cover the following topics:
- Create an app using the default template with TypeScript enabled
- Implement a two-screen bottom tabs layout with Expo Router
- Break down the app layout and implement it with flexbox
- Use each platform's system UI to select an image from the media library
- Create a sticker modal using the `` and `` components from React Native
- Add touch gestures to interact with a sticker
- Use third-party libraries to capture a screenshot and save it to the disk
- Handle platform differences between Android, iOS, and web
- Finally, go through the process of configuring a status bar, a splash screen, and an icon to complete the app
These topics provide a foundation to learn the fundamentals of building an Expo app. The tutorial is self-paced and can take up to two hours to complete.
To keep it beginner friendly, we divided the tutorial into nine chapters so that you can follow along or put it down and come back to it later. Each chapter contains the necessary code snippets to complete the steps, so you can follow along by creating an app from scratch or copy and paste it.
Before we get started, take a look at what we'll build. It's an app named **StickerSmash** that runs on Android, iOS, and the web:
> The complete source code for this tutorial is available on [GitHub](https://github.com/expo/examples/tree/master/stickersmash).
## How to use this tutorial
We believe in [learning by doing](https://en.wikipedia.org/wiki/Learning-by-doing), so this tutorial emphasizes doing over explaining. You can follow along the journey of building an app by creating the app from scratch.
Throughout the tutorial, any important code or code that has changed between examples will be highlighted in green. You can hover over the highlights (on desktop) or tap them (on mobile) to learn more about the change. For example, the code highlighted in the snippet below explains what it does:
```tsx
import { StyleSheet, Text, View } from 'react-native';
export default function Index() {
return (
Hello world!
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
```
## Next step
We're ready to start building our app.
[Start](/tutorial/create-your-first-app) — Let's start by creating a new Expo app.
---
---
modificationDate: February 25, 2026
title: Create your first app
description: In this chapter, learn how to create a new Expo project.
---
# Create your first app
In this chapter, learn how to create a new Expo project.
In this chapter, let's learn how to create a new Expo project and how to get it running.
[Watch: Creating your first universal Expo app](https://www.youtube.com/watch?v=m1-bc53EGh8)
## Prerequisites
We'll need the following to get started:
- [Expo Go](https://expo.dev/go) installed on a physical device
- [Node.js (LTS version)](https://nodejs.org/en) installed
- [VS Code](https://code.visualstudio.com/) or any other preferred code editor or IDE installed
- A macOS, Linux, or Windows (PowerShell and [WSL2](https://expo.fyi/wsl)) with a terminal window open
The tutorial assumes that you are familiar with TypeScript and React. If you are not familiar with them, check out the [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html) and [React's official tutorial](https://react.dev/learn).
## Initialize a new Expo app
We'll use [`create-expo-app`](/more/create-expo) to initialize a new Expo app. It is a command line tool to create a new React Native project. Run the following command in your terminal:
```sh
npx create-expo-app@latest StickerSmash
cd StickerSmash
```
This command will create a new project directory named StickerSmash, using the [default](/more/create-expo#--template) template. This template has essential boilerplate code and libraries needed to build our app, including Expo Router. We'll continue to add more libraries throughout this tutorial as needed.
Benefits of using the default template
- Creates a new React Native project with `expo` package installed
- Includes recommended tools such as Expo CLI
- Includes a tab navigator from Expo Router to provide a basic navigation system
- Automatically configured to run a project on multiple platforms: Android, iOS, and web
- TypeScript configured by default
## Download assets
[Download assets archive](/static/images/tutorial/sticker-smash-assets.zip) — We'll be using these assets throughout this tutorial.
After downloading the archive:
1. Unzip the archive and replace the default assets in the **your-project-name/assets/images** directory.
2. Open the project directory in a code editor or IDE.
## Run reset-project script
In this tutorial, we'll build our app from scratch and understand the fundamentals of adding a file-based navigation. Let's run the `reset-project` script to remove the boilerplate code:
```sh
npm run reset-project
```
After running the above command, there are two files (**index.tsx** and **_layout.tsx**) left inside the **app** directory. The previous files from **app** and other directories (**components**, **constants**, and **hooks** — containing boilerplate code) are moved inside the **app-example** directory by the script. We'll create our own directories and component files as we go along.
What does the `reset-project` script do?
`reset-project` script resets the **app** directory structure in a project and copies the previous boilerplate files from the project's root directory to another sub-directory called **app-example**. We can delete it since it is not part of our main app's structure.
## Run the app on mobile and web
In the project directory, run the following command to start the [development server](/more/glossary-of-terms#development-server) from the terminal:
```sh
npx expo start
```
After running the above command:
1. The development server will start, and you'll see a QR code inside the terminal window.
2. Scan that QR code to open the app on the device. On Android, use the Expo Go > **Scan QR code** option. On iOS, use the default camera app.
3. To run the web app, press w in the terminal. It will open the web app in the default web browser.
Once it is running on all platforms, the app should look like this:
## Edit the index screen
The **app/index.tsx** file defines the text displayed on the app's screen. It is the entry point of our app and executes when the development server starts. It uses core React Native components such as `` and `` to display background and text.
Styles applied to these components use JavaScript objects rather than CSS, which is used on web. However, a lot of the properties will look familiar if you've previously used CSS on web. Most React Native components accept a `style` prop that accepts a JavaScript object as its value. For more details, see [Styling in React Native](https://reactnative.dev/docs/style).
Let's modify **app/index.tsx** screen:
1. Import `StyleSheet` from `react-native` and create a `styles` object to define our custom styles.
2. Add a `styles.container.backgroundColor` property to `` with the value of `#25292e`. This changes the background color.
3. Replace the default value of `` with "Home screen".
4. Add a `styles.text.color` property to `` with the value of `#fff` (white) to change the text color.
```tsx
import { Text, View, StyleSheet } from 'react-native';
export default function Index() {
return (
Home screen
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#25292e',
alignItems: 'center',
justifyContent: 'center',
},
text: {
color: '#fff',
},
});
```
> React Native uses the same color format as the web. It supports hex triplets (this is what `#fff` is), `rgba`, `hsl`, and named colors, such as `red`, `green`, `blue`, `peru`, and `papayawhip`. For more information, see [Colors in React Native](https://reactnative.dev/docs/colors).
Once you save your changes, they're sent and applied to the running apps connected to the development server:
## Summary
Chapter 1: Create your first app
We've successfully created a new Expo project, used React Native core components, and are ready to develop our StickerSmash app.
In the next chapter, we will learn how to add a stack and a tab navigator to our app.
[Next: Add navigation](/tutorial/add-navigation)
---
---
modificationDate: February 25, 2026
title: Add navigation
description: In this chapter, learn how to add navigation to the Expo app.
---
# Add navigation
In this chapter, learn how to add navigation to the Expo app.
In this chapter, we'll learn Expo Router's fundamentals to create stack navigation and a bottom tab bar with two tabs.
[Watch: Adding navigation in your universal Expo app](https://www.youtube.com/watch?v=8336fcFV_T4)
## Expo Router basics
Expo Router is a file-based routing framework for React Native and web apps. It manages navigation between screens and uses the same components across multiple platforms. To get started, we need to know about the following conventions:
- **app directory**: A special directory containing only routes and their layouts. Any files added to this directory become a screen inside our native app and a page on the web.
- **Root layout**: The **app/_layout.tsx** file. It defines shared UI elements such as headers and tab bars so they are consistent between different routes.
- **File name conventions**: _Index_ file names, such as **index.tsx**, match their parent directory and do not add a path segment. For example, the **index.tsx** file in the **app** directory matches `/` route.
- A **route** file exports a React component as its default value. It can use either `.js`, `.jsx`, `.ts`, or `.tsx` extension.
- Android, iOS, and web share a unified navigation structure.
> The above list is enough for us to get started. For a complete list of features, see [Introduction to Expo Router](/router/introduction).
## Add a new screen to the stack
Let's create a new file named **about.tsx** inside the **app** directory. It displays the screen name when the user navigates to the `/about` route.
```tsx
import { Text, View, StyleSheet } from 'react-native';
export default function AboutScreen() {
return (
About screen
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#25292e',
justifyContent: 'center',
alignItems: 'center',
},
text: {
color: '#fff',
},
});
```
Inside **app/_layout.tsx**:
1. Add a `` component and an `options` prop to update the title of the `/about` route.
2. Update the `/index` route's title to `Home` by adding `options` prop.
```tsx
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
);
}
```
What is a `Stack`?
A stack navigator is the foundation for navigating between different screens in an app. On Android, a stacked route animates on top of the current screen. On iOS, a stacked route animates from the right. Expo Router provides a `Stack` component to create a navigation stack to add new routes.
## Navigate between screens
We'll use Expo Router's `Link` component to navigate from the `/index` route to the `/about` route. It is a React component that renders a `` with a given `href` prop.
1. Import the `Link` component from `expo-router` inside **index.tsx**.
2. Add a `Link` component after `` component and pass `href` prop with the `/about` route.
3. Add a style of `fontSize`, `textDecorationLine`, and `color` to `Link` component. It takes the same props as the `` component.
```tsx
import { Text, View, StyleSheet } from 'react-native';
import { Link } from 'expo-router';
export default function Index() {
return (
Home screen
Go to About screen
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#25292e',
alignItems: 'center',
justifyContent: 'center',
},
text: {
color: '#fff',
},
button: {
fontSize: 20,
textDecorationLine: 'underline',
color: '#fff',
},
});
```
Let's take a look at the changes in our app. Click on `Link` to navigate to the `/about` route:
## Add a not-found route
When a route doesn't exist, we can use a `+not-found` route to display a fallback screen. This is useful when we want to display a custom screen when navigating to an invalid route on mobile instead of crashing the app or display a _404_ error on web. Expo Router uses a special **+not-found.tsx** file to handle this case.
1. Create a new file named **+not-found.tsx** inside the app directory to add the `NotFoundScreen` component.
2. Add `options` prop from the `Stack.Screen` to display a custom screen title for this route.
3. Add a `Link` component to navigate to the `/` route, which is our fallback route.
```tsx
import { View, StyleSheet } from 'react-native';
import { Link, Stack } from 'expo-router';
export default function NotFoundScreen() {
return (
<>
Go back to Home screen!
>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#25292e',
justifyContent: 'center',
alignItems: 'center',
},
button: {
fontSize: 20,
textDecorationLine: 'underline',
color: '#fff',
},
});
```
To test this, navigate to `http:localhost:8081/123` URL in the web browser since it is easy to change the URL path there. The app should display the `NotFoundScreen` component:
## Add a bottom tab navigator
At this point, the file structure of our **app** directory looks like the following:
`app`
`_layout.tsx``Root layout`
`index.tsx``matches route '/'`
`about.tsx``matches route '/about'`
`+not-found.tsx``matches route any 404 route`
We'll add a bottom tab navigator to our app and reuse the existing Home and About screens to create a tab layout (a common navigation pattern in many social media apps like X or BlueSky). We'll also use the stack navigator in the Root layout so the `+not-found` route displays over any other nested navigators.
1. Inside the **app** directory, add a **(tabs)** subdirectory. This special directory is used to group routes together and display them in a bottom tab bar.
2. Create a **(tabs)/_layout.tsx** file inside the directory. It will be used to define the tab layout, which is separate from Root layout.
3. Move the existing **index.tsx** and **about.tsx** files inside the **(tabs)** directory. The structure of **app** directory will look like this:
`app`
`_layout.tsx``Root layout`
`+not-found.tsx``matches route any 404 route`
`(tabs)`
`_layout.tsx``Tab layout`
`index.tsx``matches route '/'`
`about.tsx``matches route '/about'`
Update the Root layout file to add a `(tabs)` route:
```tsx
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
);
}
```
Inside **(tabs)/_layout.tsx**, add a `Tabs` component to define the bottom tab layout:
```tsx
import { Tabs } from 'expo-router';
export default function TabLayout() {
return (
);
}
```
Let's take a look at our app now to see the new bottom tabs:
## Update bottom tab navigator appearance
Right now, the bottom tab navigator looks the same on all platforms but doesn't match the style of our app. For example, the tab bar or header doesn't display a custom icon, and the bottom tab background color doesn't match the app's background color.
Modify the **(tabs)/_layout.tsx** file to add tab bar icons:
1. Import `Ionicons` icons set from [`@expo/vector-icons`](/guides/icons#expovector-icons) — a library that includes popular icon sets.
2. Add the `tabBarIcon` to both the `index` and `about` routes. This function takes `focused` and `color` as params and renders the icon component. From the icon set, we can provide custom icon names.
3. Add `screenOptions.tabBarActiveTintColor` to the `Tabs` component and set its value to `#ffd33d`. This will change the color of the tab bar icon and label when active.
```tsx
import { Tabs } from 'expo-router';
import Ionicons from '@expo/vector-icons/Ionicons';
export default function TabLayout() {
return (
(
),
}}
/>
(
),
}}
/>
);
}
```
Let's also change the background color of the tab bar and header using `screenOptions` prop:
```tsx
```
In the above code:
- The header's background is set to `#25292e` using the `headerStyle` property. We have also disabled the header's shadow using `headerShadowVisible`.
- `headerTintColor` applies `#fff` to the header label
- `tabBarStyle.backgroundColor` applies `#25292e` to the tab bar
Our app now has a custom bottom tabs navigator:
## Summary
Chapter 2: Add navigation
We've successfully added a stack and a tab navigator to our app.
In the next chapter, we'll learn how to build the app's first screen.
[Next: Build your app's first screen](/tutorial/build-a-screen)
---
---
modificationDate: February 25, 2026
title: Build a screen
description: In this tutorial, learn how to use components such as React Native's Pressable and Expo Image to build a screen.
---
# Build a screen
In this tutorial, learn how to use components such as React Native's Pressable and Expo Image to build a screen.
In this chapter, we'll create the first screen of the StickerSmash app.
The screen above displays an image and two buttons. The app user can select an image using one of the two buttons. The first button allows the user to select an image from their device. The second button allows the user to continue with a default image provided by the app.
Once the user selects an image, they can add a sticker to it. So, let's start creating this screen.
[Watch: Building a screen in your universal Expo app](https://www.youtube.com/watch?v=3rcOP8xDwTQ)
## Break down the screen
Before we build this screen by writing code, let's break it down into some essential elements.
There are two essential elements:
- There is a large image displayed at the center of the screen
- There are two buttons in the bottom half of the screen
The first button contains multiple components. The parent element provides a yellow border, and contains an icon and text components inside a row.
Now that we've broken down the UI into smaller chunks, we're ready to start coding.
## Display the image
We'll use `expo-image` library to display the image in the app. It provides a cross-platform `` component to load and render an image. It is already included in the default project template we're using.
The Image component takes the source of an image as its value. The source can be either a [static asset](https://reactnative.dev/docs/images#static-image-resources) or a URL. For example, the source required from **assets/images** directory is static. It can also come from [Network](https://reactnative.dev/docs/images#network-images) as a `uri` property.
To use the Image component in **app/(tabs)/index.tsx** file:
1. Import `Image` from the `expo-image` library.
2. Create a `PlaceholderImage` variable to use **assets/images/background-image.png** file as the `source` prop on the `Image` component.
```tsx
import { View, StyleSheet } from 'react-native';
import { Image } from 'expo-image';
const PlaceholderImage = require('@/assets/images/background-image.png');
export default function Index() {
return (
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#25292e',
alignItems: 'center',
},
imageContainer: {
flex: 1,
},
image: {
width: 320,
height: 440,
borderRadius: 18,
},
});
```
## Divide components into files
Let's divide the code into multiple files as we add more components to this screen. Throughout this tutorial, we'll use the components directory to create custom components.
1. Create a top-level **components** directory, and inside it, create the **ImageViewer.tsx** file.
2. Move the code to display the image in this file along with the `image` styles.
```tsx
import { ImageSourcePropType, StyleSheet } from 'react-native';
import { Image } from 'expo-image';
type Props = {
imgSource: ImageSourcePropType;
};
export default function ImageViewer({ imgSource }: Props) {
return ;
}
const styles = StyleSheet.create({
image: {
width: 320,
height: 440,
borderRadius: 18,
},
});
```
> Since **ImageViewer** is a custom component, we are placing it in a separate directory instead of the **app** directory. Every file inside **app** directory is either a layout file or a route file. For more information, see [Non-navigation components live outside of app directory](/router/basics/core-concepts#5-non-navigation-components-live-outside-of-app-directory).
Import `ImageViewer` and use it in the **app/(tabs)/index.tsx**:
```tsx
import { StyleSheet, View } from 'react-native';
import ImageViewer from '@/components/ImageViewer';
const PlaceholderImage = require('@/assets/images/background-image.png');
export default function Index() {
return (
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#25292e',
alignItems: 'center',
},
imageContainer: {
flex: 1,
},
});
```
What is the `@` in import statement?
The `@` symbol is a custom [path alias](/guides/typescript#path-aliases-optional) for importing custom components and other modules instead of relative paths. Expo CLI automatically configures it in **tsconfig.json**.
## Create buttons using Pressable
React Native includes a few different components for handling touch events, but [``](https://reactnative.dev/docs/pressable) is recommended for its flexibility. It can detect single taps, long presses, trigger separate events when the button is pushed in and released, and more.
In the design, there are two buttons we need to create. Each has a different style and label. Let's start by creating a reusable component for these buttons. Create a **Button.tsx** file inside the **components** directory with the following code:
```tsx
import { StyleSheet, View, Pressable, Text } from 'react-native';
type Props = {
label: string;
};
export default function Button({ label }: Props) {
return (
alert('You pressed a button.')}>
{label}
);
}
const styles = StyleSheet.create({
buttonContainer: {
width: 320,
height: 68,
marginHorizontal: 20,
alignItems: 'center',
justifyContent: 'center',
padding: 3,
},
button: {
borderRadius: 10,
width: '100%',
height: '100%',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
},
buttonLabel: {
color: '#fff',
fontSize: 16,
},
});
```
The app displays an alert when the user taps any of the buttons on the screen. It happens because `` calls `alert()` on its `onPress` prop. Let's import this component into **app/(tabs)/index.tsx** file and add styles for the `` that encapsulates these buttons:
```tsx
import { View, StyleSheet } from 'react-native';
import Button from '@/components/Button';
import ImageViewer from '@/components/ImageViewer';
const PlaceholderImage = require("@/assets/images/background-image.png");
export default function Index() {
return (
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#25292e',
alignItems: 'center',
},
imageContainer: {
flex: 1,
paddingTop: 28,
},
footerContainer: {
flex: 1 / 3,
alignItems: 'center',
},
});
```
Let's take a look at our app on Android, iOS and the web:
The second button with the label "Use this photo" resembles the actual button from the design. However, the first button needs more styling to match the design.
## Enhance the reusable button component
The "Choose a photo" button requires different styling than the "Use this photo" button, so we will add a new button theme prop that will allow us to apply a `primary` theme. This button also has an icon before the label. We will use an icon from the `@expo/vector-icons` library.
To load and display the icon on the button, let's use `FontAwesome` from the library. Modify **components/Button.tsx** to add the following code snippet:
```tsx
import { StyleSheet, View, Pressable, Text } from 'react-native';
import FontAwesome from '@expo/vector-icons/FontAwesome';
type Props = {
label: string;
theme?: 'primary';
};
export default function Button({ label, theme }: Props) {
if (theme === 'primary') {
return (
alert('You pressed a button.')}>
{label}
);
}
return (
alert('You pressed a button.')}>
{label}
);
}
const styles = StyleSheet.create({
buttonContainer: {
width: 320,
height: 68,
marginHorizontal: 20,
alignItems: 'center',
justifyContent: 'center',
padding: 3,
},
button: {
borderRadius: 10,
width: '100%',
height: '100%',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
},
buttonIcon: {
paddingRight: 8,
},
buttonLabel: {
color: '#fff',
fontSize: 16,
},
});
```
Let's learn what the above code does:
- The primary theme button uses **inline styles**, which overrides the styles defined in `StyleSheet.create()` with an object directly passed in the `style` prop.
- The `` component in the primary theme uses a `backgroundColor` property with a value `#fff` to set the button's background to white. If we add this property to the `styles.button`, the background color value will be set for both the primary theme and the unstyled one.
- Inline styles use JavaScript and override the default styles for a specific value.
Now, modify the **app/(tabs)/index.tsx** file to use the `theme="primary"` prop on the first button.
```tsx
import { View, StyleSheet } from 'react-native';
import Button from '@/components/Button';
import ImageViewer from '@/components/ImageViewer';
const PlaceholderImage = require('@/assets/images/background-image.png');
export default function Index() {
return (
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#25292e',
alignItems: 'center',
},
imageContainer: {
flex: 1,
},
footerContainer: {
flex: 1 / 3,
alignItems: 'center',
},
});
```
Let's take a look at our app on Android, iOS and the web:
## Summary
Chapter 3: Build a screen
We've successfully implemented the initial design to start building our app's first screen.
In the next chapter, we'll add the functionality to pick an image from the device's media library.
[Next: Use an image picker](/tutorial/image-picker)
---
---
modificationDate: February 25, 2026
title: Use an image picker
description: In this tutorial, learn how to use Expo Image Picker.
---
# Use an image picker
In this tutorial, learn how to use Expo Image Picker.
React Native provides built-in components as standard building blocks, such as ``, ``, and ``. We are building a feature to select an image from the device's media gallery. This isn't possible with the core components and we'll need a library to add this feature in our app.
We'll use [`expo-image-picker`](/versions/latest/sdk/imagepicker), a library from Expo SDK.
> `expo-image-picker` provides access to the system's UI to select images and videos from the phone's library.
[Watch: Using an image picker in your universal Expo app](https://www.youtube.com/watch?v=iEQZU58naS8)
## Install expo-image-picker
To install the `expo-image-picker` library, stop the development server by pressing Ctrl + c in the terminal, then run the following command:
```sh
npx expo install expo-image-picker
```
The [`npx expo install`](/more/expo-cli#installation) command will install the library and add it to the project's dependencies in **package.json**.
> **Tip:** Any time we install a new library in the project, stop the development server by pressing Ctrl + c in the terminal and then run the installation command. After the installation completes, start the development server again by running `npx expo start`.
## Pick an image from the device's media library
`expo-image-picker` provides `launchImageLibraryAsync()` method to display the system UI by choosing an image or a video from the device's media library. We'll use the primary themed button created in the previous chapter to select an image from the device's media library and create a function to launch the device's image library to implement this functionality.
In **app/(tabs)/index.tsx**, import `expo-image-picker` library and create a `pickImageAsync()` function inside the `Index` component:
```tsx
// ...rest of the import statements remain unchanged
import * as ImagePicker from 'expo-image-picker';
export default function Index() {
const pickImageAsync = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ['images'],
allowsEditing: true,
quality: 1,
});
if (!result.canceled) {
console.log(result);
} else {
alert('You did not select any image.');
}
};
// ...rest of the code remains same
}
```
Let's learn what the above code does:
- The `launchImageLibraryAsync()` receives an object to specify different options. This object is the [`ImagePickerOptions`](/versions/latest/sdk/imagepicker#imagepickeroptions) object, which we are passing when invoking the method.
- When `allowsEditing` is set to `true`, the user can crop the image during the selection process on Android and iOS.
## Update the button component
On pressing the primary button, we'll call the `pickImageAsync()` function on the `Button` component. Update the `onPress` prop of the `Button` component in **components/Button.tsx**:
```tsx
import { StyleSheet, View, Pressable, Text } from 'react-native';
import FontAwesome from '@expo/vector-icons/FontAwesome';
type Props = {
label: string;
theme?: 'primary';
onPress?: () => void;
};
export default function Button({ label, theme, onPress }: Props) {
if (theme === 'primary') {
return (
{label}
);
}
return (
alert('You pressed a button.')}>
{label}
);
}
const styles = StyleSheet.create({
buttonContainer: {
width: 320,
height: 68,
marginHorizontal: 20,
alignItems: 'center',
justifyContent: 'center',
padding: 3,
},
button: {
borderRadius: 10,
width: '100%',
height: '100%',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
},
buttonIcon: {
paddingRight: 8,
},
buttonLabel: {
color: '#fff',
fontSize: 16,
},
});
```
In **app/(tabs)/index.tsx**, add the `pickImageAsync()` function to the `onPress` prop on the first `