skip to content

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.