In an Expo app, how do you read app config values at runtime through Constants.expoConfig and extra, and how does that differ from EXPO_PUBLIC_ variables?
answer
- expo-constants, nullable getter
- extra carries arbitrary data
- never import app.json directly
- some fields filtered out of public config
- npx expo config --type public
basics
~20 sImport Constants from expo-constants and read Constants.expoConfig?.extra, which holds the public part of the resolved app config. EXPO_PUBLIC_ variables are literals inlined into your code; extra is data carried in the app's embedded config or manifest.
solid answer
~40 s`Constants.expoConfig` from `expo-constants` exposes the public part of the resolved app config, and the `extra` key is where you put arbitrary values, often computed in `app.config.ts` from environment variables. Read it defensively, `Constants.expoConfig?.extra?.apiUrl`, because the getter can return `null`. Do not `import` `app.json`: that bundles the raw file, not what a dynamic config resolved to. A few fields are filtered out (`hooks`, `ios.config`, `android.config` and the update code-signing fields), and `npx expo config --type public` shows exactly what is exposed. The difference from `EXPO_PUBLIC_` variables: those are replaced by literals in your own source at bundle time, while `extra` travels as config data in the embedded config, the dev server's manifest or an update's manifest. Both are public; neither may hold secrets.
code
typescript · 8 linesimport Constants from 'expo-constants';
type Extra = { apiUrl?: string; supportPhone?: string };
const extra = (Constants.expoConfig?.extra ?? {}) as Extra;
export const apiUrl = extra.apiUrl ?? process.env.EXPO_PUBLIC_API_URL;
export const supportPhone = extra.supportPhone;go deeper
Know that Constants.expoConfig.extra from expo-constants holds custom config values, and that it may be null.
Explain where the runtime copy comes from in development, in a release build and under an update, and how extra differs from inlined EXPO_PUBLIC_ literals.
Verify what ships with npx expo config --type public, keep secrets out of both mechanisms, and trace stale values to the build or update that carried them.
Standardise one typed environment object per app so variants and libraries read the same source instead of scattered literals.
## Where Constants.expoConfig comes from `expo-constants` is the Expo module that exposes system and app information to JavaScript. Its `expoConfig` property returns the **public app config**: the resolved result of `app.json` plus any `app.config.ts`, minus a few filtered fields. Which copy you receive depends on how the app was launched: - **A release build running its embedded bundle** reads a config file that `expo-constants` generates and packages during the native build. - **A development build connected to `npx expo start`** reads the manifest the dev server serves, which is re-evaluated as Metro reloads. - **An app running an EAS Update** reads the config stored in that update's manifest, captured when the update was exported. That is why the getter is typed as nullable: there are launch situations without a manifest, and code should read `Constants.expoConfig?.extra`. ## The extra key `extra` is the app config's free-form slot. Anything you put there is passed to your app: ```ts // app.config.ts export default ({ config }) => ({ ...config, extra: { apiUrl: process.env.API_URL, supportPhone: '+1 555 0100' }, }); ``` Because `app.config.ts` runs in Expo CLI's Node process, it can read **unprefixed** environment variables and copy chosen ones into `extra`. That is the older way of passing environment values to an app, and it still works. ## What is filtered out, and how to check The public config drops fields that are meant for tooling only: - `hooks` - `ios.config` and `android.config` - `updates.codeSigningCertificate` and `updates.codeSigningMetadata` Everything else is visible at runtime, and therefore extractable from the app package. Run `npx expo config --type public` to print exactly what will be embedded and exposed. Separately, **do not import `app.json` or `app.config.js` into app code**: an import bundles the raw file, which misses whatever a dynamic config computed and drags the whole file into the bundle. ## extra versus EXPO_PUBLIC_ | | `Constants.expoConfig.extra` | `process.env.EXPO_PUBLIC_*` | |---|---|---| | Mechanism | config data in a manifest or embedded config | literal text substituted into your code | | Where values come from | whatever `app.config.ts` computes | `.env` files and the bundling environment | | Reachable from `node_modules` | yes, any code can read `Constants` | no, library code is not rewritten | | Needs a module | `expo-constants` | none | | Visibility | public | public | The Expo environment-variables guide suggests moving references from `Constants.expoConfig.extra` straight to `EXPO_PUBLIC_` variables when the value is only used in your code. `extra` still earns its place when: 1. the value is **derived** from several inputs in the config itself, such as a variant name; 2. a **library** needs to read it, since libraries are not inlined; 3. you want **one typed object** describing the environment rather than scattered literals. ## Reading it in practice A tidy pattern is one small module that owns the read: 1. Import `Constants` from `expo-constants` once, in a file such as `env.ts`. 2. Cast `Constants.expoConfig?.extra` to a declared type, because `extra` is typed loosely. 3. Validate the fields you need and throw a clear error at startup if one is missing. 4. Export typed constants; screens and API clients import those, never `Constants` directly. This keeps the nullable getter and the loose typing in one place and gives tests a single seam to replace. It also means that when a car-sharing app's booking screen talks to the wrong host, there is exactly one file to inspect, and `npx expo config --type public` shows what that file will receive. ## Pitfalls - Reading `Constants.expoConfig.extra.apiUrl` without optional chaining crashes if the getter returns `null`. - Assuming a changed `extra` value reaches installed apps without a new build or update: the embedded copy is fixed at build time. - Putting a secret into `extra` because it is not in the source code: it is in the public config, which is inside the app.
- A value added to extra shows up in development but not in the installed release build; why?In development `Constants.expoConfig` comes from the dev server's freshly evaluated manifest. A release build running its embedded bundle reads the config packaged when that native build was made, so the new `extra` value appears only after a new build, or in a newer update whose manifest carries it.
- When is extra a better fit than an EXPO_PUBLIC_ variable?When a library in `node_modules` must read the value, because inlining skips `node_modules` while `Constants` is readable anywhere; or when the value is derived inside `app.config.ts` from several inputs, such as a variant name, and you want one typed object rather than several literals.
saying these in an interview costs you the question
- Importing app.json is equivalent to reading Constants.expoConfig
- Values in extra are private because they are not in the source
- Constants.expoConfig is always non-null, no optional chaining needed
- Every app config field is exposed through Constants.expoConfig
- extra is re-read from the server on every launch