skip to content

Mods & Modifiers

A plugin is a function over the config that registers mods like withInfoPlist or withAndroidManifest, with withDangerousMod for raw file edits. Interviewers check idempotency and fragile regex edits.

part ofExpo (React Native)overview, primer and where to startread it →
on this pageshow

explore

questions

5

In an Expo project, what is a config plugin function, and how does a mod plugin like withInfoPlist change a native file?

level: juniorimportance: must knowfreq 42%

answer

  1. a function over the app config
  2. (config, props) => config, named with…
  3. registers a mod, runs later
  4. modResults in, config out
  5. prebuild, then a new native build

basics

~20 s

An Expo config plugin is a synchronous function that takes the app config and optional props and returns the config. A mod plugin like withInfoPlist registers an async callback that edits the parsed Info.plist during npx expo prebuild.

solid answer

~40 s

A config plugin is a `ConfigPlugin<Props>` from `expo/config-plugins`: `(config, props) => config`, conventionally named `withSomething`. It runs every time the app config is evaluated, so it stays synchronous and never touches files itself. To change a native file it calls a **mod plugin** such as `withInfoPlist`, `withEntitlementsPlist` or `withAndroidManifest`, which registers an async **mod** on the config. Mods run only in the syncing phase of `npx expo prebuild` (locally or in EAS Build's prebuild step): the compiler reads the file, hands the callback `config.modResults` (the plist or manifest parsed into a JS object) and `config.modRequest` (project paths, platform, mod name), then writes back whatever `modResults` the callback returns. The callback must return the config. Users see the change only after a new native build.

code

typescript · 12 lines
typescript
import { ConfigPlugin, withInfoPlist } from 'expo/config-plugins';

type Props = { region: string };

const withApiRegion: ConfigPlugin<Props> = (config, { region }) => {
  return withInfoPlist(config, (config) => {
    config.modResults.ApiRegion = region;
    return config;
  });
};

export default withApiRegion;

go deeper

for a junior

Recall the shape: a function from config to config, named with a with prefix, that uses mod plugins such as withInfoPlist to edit native files during prebuild.

for a middle

Explain the two phases: plugins run at config evaluation and only register mods, and the mod compiler later reads each file, runs the callbacks on modResults and writes the result.

for a senior

Show you keep config edits outside mods, prefer typed mod plugins over text edits, and know that a plugin change needs a new native build, never an OTA update.

for a principal

Frame config plugins as the contract that lets native folders stay generated: weigh how many plugins a team maintains against the upgrade cost each one adds per SDK release.

## What a config plugin is An Expo project that uses **Continuous Native Generation** does not keep hand-edited `ios/` and `android/` folders as its source of truth; `npx expo prebuild` generates them from a template plus the app config. A **config plugin** is how a project or a library says "and also change this native file" in a repeatable way. Technically a plugin is just a function with the type `ConfigPlugin<Props>` exported from `expo/config-plugins` (the `expo` package re-exports `@expo/config-plugins` under that path): - It takes the **app config** (`ExpoConfig`) and an optional second argument, the **props**. - It returns the (possibly modified) config. - It is **synchronous** and should be fast, because it runs whenever the config is evaluated, not only at prebuild. - By convention it is named `with<Feature>`, for example `withApiRegion`. - Props must be **static values** (strings, numbers, booleans, arrays, plain objects), because the app config must serialize to JSON. A plugin may change plain config fields directly (`config.ios.bundleIdentifier`), but it cannot open a native file from its body. For that it uses a mod plugin. ## Two phases: config evaluation and mod compilation The key mental model is that a plugin **registers** work and prebuild **performs** it later: 1. **Config evaluation.** Expo CLI loads `app.json` or `app.config.ts` and applies every plugin. A call such as `withInfoPlist(config, callback)` does not run the callback; it stores it on the non-serialized `config.mods.ios.infoPlist` chain. 2. **Mod compilation.** During the syncing phase of `npx expo prebuild`, the mod compiler adds a **base mod** for each file. The base mod reads the file from disk (or the template), runs the registered callbacks, checks that each returned a valid config, then writes the result back. 3. **Native build.** The edited project is compiled by Xcode or Gradle, locally or on EAS Build. Because mods only run in step 2, anything that should be visible outside prebuild (for example to `npx expo config` or to the manifest the dev server serves) belongs in the plugin body, not inside a mod. ## What a mod callback receives | Property | What it holds | |---|---| | `config.modResults` | The file's data, typed per mod: an `InfoPlist` object, an `AndroidManifest` object parsed with xml2js, a properties list, or a string | | `config.modRequest.projectRoot` | The project root, where `package.json` lives | | `config.modRequest.platformProjectRoot` | The `ios/` or `android/` folder | | `config.modRequest.platform` / `modName` | Which mod is running, for example `ios` and `infoPlist` | | `config.modRequest.projectName` | iOS only: the folder name under `ios/` | | `config.modRequest.introspect` | `true` when results are only being previewed, not written | The callback edits `config.modResults` and **returns `config`**. If it returns nothing, the compiler's sanity check throws a "not a valid project config" error naming the mod. ## The common mod plugins | Mod plugin | File | Data you edit | |---|---|---| | `withInfoPlist` | `ios/<name>/Info.plist` | JS object, merged with `ios.infoPlist` from app config | | `withEntitlementsPlist` | the app target's `.entitlements` | JS object, kept in sync with `ios.entitlements` | | `withAndroidManifest` | `android/app/src/main/AndroidManifest.xml` | JS object from xml2js | | `withGradleProperties` | `android/gradle.properties` | list of property items | | `withPodfileProperties` | `ios/Podfile.properties.json` | JSON | | `withDangerousMod` | any file, by path | nothing: you read and write the file yourself | The docs flag the string-based mods (`withAppDelegate`, `withMainApplication`, `withAppBuildGradle` and similar) and `withDangerousMod` as risky, because they rely on text matching. ## Composing plugins A feature usually needs an iOS half and an Android half. Write each as its own plugin function and compose them in a top-level plugin, either by calling them in sequence or with `withPlugins(config, [pluginA, [pluginB, props]])`, which applies a list of plugins in order. Libraries expose the top-level plugin from `app.plugin.js`; how a project lists and orders plugins is a separate topic. ## When the change reaches the app - A JavaScript reload re-runs only JS; it never re-runs mods. - The change appears after `npx expo prebuild` and a **new native build** (a development build or an EAS Build). - For the same reason an over-the-air update cannot deliver a config plugin change: it ships JavaScript and assets, not native project edits.

  • Why should an Expo config plugin change app-config fields such as ios.bundleIdentifier outside a mod rather than inside one?
    Mods run only in prebuild's syncing phase, while the plugin body runs on every config evaluation: `npx expo config`, the dev server's manifest, EAS reading the project. A config field set inside a mod is invisible to all of those consumers. So plain config changes go in the plugin body, and only native file edits go inside a mod callback.
  • Why must the props passed to an Expo config plugin be static values rather than functions or promises?
    The app config, including the plugins array and its props, has to serialize to JSON so it can be used as the app manifest. A function or promise cannot be serialized. Expo's guidance is to keep props to strings, numbers, booleans, arrays and plain objects, and to prefer good defaults over required options.
  • A developer adds a config plugin, reloads the app, and the new Info.plist key is missing; what did they skip?
    Reloading re-runs only JavaScript. The plugin edits native project files, so they must run `npx expo prebuild` (or let EAS Build do it) and install a new native build. The same reason means an over-the-air update cannot deliver the change.

saying these in an interview costs you the question

  • A config plugin runs on the device when the app starts.
  • Mod callbacks run every time Metro rebuilds the JavaScript bundle.
  • Mutating modResults is enough; the mod callback need not return config.
  • A config plugin change can reach users through an over-the-air update.
  • Plugin props can be functions because app.config.ts is JavaScript.
open as a page

How would you write an Expo config plugin that adds an iOS App Groups entitlement and an Android manifest meta-data entry idempotently?

level: middleimportance: should knowfreq 28%

basics

~10 s

Write two plugin functions and compose them: withEntitlementsPlist adds the group to the com.apple.security.application-groups array only if missing, and withAndroidManifest uses AndroidConfig.Manifest.addMetaDataItemToMainApplication, which updates an existing entry instead of duplicating it.

open as a page

In an Expo project, how do you see what your config plugin's mods will produce without generating the ios and android folders?

level: middleimportance: should knowfreq 16%

basics

~10 s

Run npx expo config --type introspect: it evaluates the introspectable mods, such as Info.plist, entitlements and the Android manifest, in memory and prints their results under _internal.modResults without writing any native files.

open as a page

In Expo config plugins, what does createRunOncePlugin do, and why do libraries wrap their exported plugin with it?

level: middleimportance: nice to knowfreq 11%

basics

~20 s

createRunOncePlugin wraps a plugin with withRunOnce: if its name is already in the config's _internal.pluginHistory it returns the config unchanged, otherwise it records the name and version and runs. Libraries use it so their plugin never applies twice.

open as a page

An Expo config plugin uses withDangerousMod to regex-insert a line into ios/Podfile, and each prebuild without --clean adds another copy; why, and how do you fix it?

level: seniorimportance: nice to knowfreq 20%

basics

~20 s

Without --clean, prebuild edits the Podfile already on disk, and the dangerous mod re-inserts its line because nothing checks it is present. Fix it with a guarded, tagged insert or, better, a non-dangerous mod such as withPodfileProperties.

open as a page